claude_swarm 0.1.19 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -63
  3. data/.rubocop_todo.yml +11 -0
  4. data/CHANGELOG.md +110 -0
  5. data/CLAUDE.md +64 -2
  6. data/README.md +190 -28
  7. data/Rakefile +1 -1
  8. data/examples/mixed-provider-swarm.yml +23 -0
  9. data/examples/monitoring-demo.yml +4 -4
  10. data/lib/claude_swarm/claude_code_executor.rb +7 -13
  11. data/lib/claude_swarm/claude_mcp_server.rb +26 -17
  12. data/lib/claude_swarm/cli.rb +384 -265
  13. data/lib/claude_swarm/commands/ps.rb +22 -24
  14. data/lib/claude_swarm/commands/show.rb +45 -63
  15. data/lib/claude_swarm/configuration.rb +137 -8
  16. data/lib/claude_swarm/mcp_generator.rb +39 -15
  17. data/lib/claude_swarm/openai/chat_completion.rb +264 -0
  18. data/lib/claude_swarm/openai/executor.rb +301 -0
  19. data/lib/claude_swarm/openai/responses.rb +338 -0
  20. data/lib/claude_swarm/orchestrator.rb +221 -45
  21. data/lib/claude_swarm/process_tracker.rb +7 -7
  22. data/lib/claude_swarm/session_cost_calculator.rb +93 -0
  23. data/lib/claude_swarm/session_path.rb +3 -5
  24. data/lib/claude_swarm/system_utils.rb +16 -0
  25. data/lib/claude_swarm/templates/generation_prompt.md.erb +230 -0
  26. data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
  27. data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
  28. data/lib/claude_swarm/tools/task_tool.rb +43 -0
  29. data/lib/claude_swarm/version.rb +1 -1
  30. data/lib/claude_swarm/worktree_manager.rb +145 -48
  31. data/lib/claude_swarm.rb +34 -12
  32. data/llms.txt +2 -2
  33. data/single.yml +482 -6
  34. data/team.yml +344 -0
  35. metadata +65 -14
  36. data/claude-swarm.yml +0 -64
  37. data/lib/claude_swarm/reset_session_tool.rb +0 -22
  38. data/lib/claude_swarm/session_info_tool.rb +0 -22
  39. data/lib/claude_swarm/task_tool.rb +0 -39
  40. /data/{example → examples}/claude-swarm.yml +0 -0
  41. /data/{example → examples}/microservices-team.yml +0 -0
  42. /data/{example → examples}/session-restoration-demo.yml +0 -0
  43. /data/{example → examples}/test-generation.yml +0 -0
@@ -1,109 +1,190 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thor"
4
- require "json"
5
- require_relative "configuration"
6
- require_relative "mcp_generator"
7
- require_relative "orchestrator"
8
- require_relative "claude_mcp_server"
9
-
10
3
  module ClaudeSwarm
11
4
  class CLI < Thor
12
- def self.exit_on_failure?
13
- true
5
+ include SystemUtils
6
+ class << self
7
+ def exit_on_failure?
8
+ true
9
+ end
14
10
  end
15
11
 
16
12
  desc "start [CONFIG_FILE]", "Start a Claude Swarm from configuration file"
17
- method_option :config, aliases: "-c", type: :string, default: "claude-swarm.yml",
18
- desc: "Path to configuration file"
19
- method_option :vibe, type: :boolean, default: false,
20
- desc: "Run with --dangerously-skip-permissions for all instances"
21
- method_option :prompt, aliases: "-p", type: :string,
22
- desc: "Prompt to pass to the main Claude instance (non-interactive mode)"
23
- method_option :stream_logs, type: :boolean, default: false,
24
- desc: "Stream session logs to stdout (only works with -p)"
25
- method_option :debug, type: :boolean, default: false,
26
- desc: "Enable debug output"
27
- method_option :session_id, type: :string,
28
- desc: "Resume a previous session by ID or path"
29
- method_option :worktree, type: :string, aliases: "-w",
30
- desc: "Create instances in Git worktrees with the given name (auto-generated if true)",
31
- 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"
32
37
  def start(config_file = nil)
33
- # Handle session restoration
34
- if options[:session_id]
35
- restore_session(options[:session_id])
36
- return
37
- end
38
-
39
- config_path = config_file || options[:config]
38
+ config_path = config_file || "claude-swarm.yml"
40
39
  unless File.exist?(config_path)
41
- error "Configuration file not found: #{config_path}"
42
- exit 1
40
+ error("Configuration file not found: #{config_path}")
41
+ exit(1)
43
42
  end
44
43
 
45
- say "Starting Claude Swarm from #{config_path}..." unless options[:prompt]
44
+ say("Starting Claude Swarm from #{config_path}...") unless options[:prompt]
46
45
 
47
46
  # Validate stream_logs option
48
47
  if options[:stream_logs] && !options[:prompt]
49
- error "--stream-logs can only be used with -p/--prompt"
50
- exit 1
48
+ error("--stream-logs can only be used with -p/--prompt")
49
+ exit(1)
51
50
  end
52
51
 
53
52
  begin
54
- config = Configuration.new(config_path, base_dir: Dir.pwd)
53
+ config = Configuration.new(config_path, base_dir: Dir.pwd, options: options)
55
54
  generator = McpGenerator.new(config, vibe: options[:vibe])
56
- orchestrator = Orchestrator.new(config, generator,
57
- vibe: options[:vibe],
58
- prompt: options[:prompt],
59
- stream_logs: options[:stream_logs],
60
- debug: options[:debug],
61
- 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
+ )
62
65
  orchestrator.start
63
66
  rescue Error => e
64
- error e.message
65
- exit 1
67
+ error(e.message)
68
+ exit(1)
66
69
  rescue StandardError => e
67
- error "Unexpected error: #{e.message}"
68
- error e.backtrace.join("\n") if options[:verbose]
69
- exit 1
70
+ error("Unexpected error: #{e.message}")
71
+ error(e.backtrace.join("\n")) if options[:verbose]
72
+ exit(1)
70
73
  end
71
74
  end
72
75
 
73
76
  desc "mcp-serve", "Start an MCP server for a Claude instance"
74
- method_option :name, aliases: "-n", type: :string, required: true,
75
- desc: "Instance name"
76
- method_option :directory, aliases: "-d", type: :string, required: true,
77
- desc: "Working directory for the instance"
78
- method_option :directories, type: :array,
79
- desc: "All directories (including main directory) for the instance"
80
- method_option :model, aliases: "-m", type: :string, required: true,
81
- desc: "Claude model to use (e.g., opus, sonnet)"
82
- method_option :prompt, aliases: "-p", type: :string,
83
- desc: "System prompt for the instance"
84
- method_option :description, type: :string,
85
- desc: "Description of the instance's role"
86
- method_option :allowed_tools, aliases: "-t", type: :array,
87
- desc: "Allowed tools for the instance"
88
- method_option :disallowed_tools, type: :array,
89
- desc: "Disallowed tools for the instance"
90
- method_option :connections, type: :array,
91
- desc: "Connections to other instances"
92
- method_option :mcp_config_path, type: :string,
93
- desc: "Path to MCP configuration file"
94
- method_option :debug, type: :boolean, default: false,
95
- desc: "Enable debug output"
96
- method_option :vibe, type: :boolean, default: false,
97
- desc: "Run with --dangerously-skip-permissions"
98
- method_option :calling_instance, type: :string, required: true,
99
- desc: "Name of the instance that launched this MCP server"
100
- method_option :calling_instance_id, type: :string,
101
- desc: "Unique ID of the instance that launched this MCP server"
102
- method_option :instance_id, type: :string,
103
- desc: "Unique ID of this instance"
104
- method_option :claude_session_id, type: :string,
105
- 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"
106
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
+
107
188
  instance_config = {
108
189
  name: options[:name],
109
190
  directory: options[:directory],
@@ -117,33 +198,42 @@ module ClaudeSwarm
117
198
  mcp_config_path: options[:mcp_config_path],
118
199
  vibe: options[:vibe] || false,
119
200
  instance_id: options[:instance_id],
120
- 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],
121
208
  }
122
209
 
123
210
  begin
124
211
  server = ClaudeMcpServer.new(
125
212
  instance_config,
126
213
  calling_instance: options[:calling_instance],
127
- calling_instance_id: options[:calling_instance_id]
214
+ calling_instance_id: options[:calling_instance_id],
128
215
  )
129
216
  server.start
130
217
  rescue StandardError => e
131
- error "Error starting MCP server: #{e.message}"
132
- error e.backtrace.join("\n") if options[:debug]
133
- exit 1
218
+ error("Error starting MCP server: #{e.message}")
219
+ error(e.backtrace.join("\n")) if options[:debug]
220
+ exit(1)
134
221
  end
135
222
  end
136
223
 
137
224
  desc "init", "Initialize a new claude-swarm.yml configuration file"
138
- method_option :force, aliases: "-f", type: :boolean, default: false,
139
- desc: "Overwrite existing configuration file"
225
+ method_option :force,
226
+ aliases: "-f",
227
+ type: :boolean,
228
+ default: false,
229
+ desc: "Overwrite existing configuration file"
140
230
  def init
141
231
  config_path = "claude-swarm.yml"
142
232
 
143
233
  if File.exist?(config_path) && !options[:force]
144
- error "Configuration file already exists: #{config_path}"
145
- error "Use --force to overwrite"
146
- exit 1
234
+ error("Configuration file already exists: #{config_path}")
235
+ error("Use --force to overwrite")
236
+ exit(1)
147
237
  end
148
238
 
149
239
  template = <<~YAML
@@ -160,7 +250,8 @@ module ClaudeSwarm
160
250
  description: "Lead developer who coordinates the team and makes architectural decisions"
161
251
  directory: .
162
252
  model: sonnet
163
- prompt: "You are the lead developer coordinating the team"
253
+ prompt: |
254
+ You are the lead developer coordinating the team
164
255
  allowed_tools: [Read, Edit, Bash, Write]
165
256
  # connections: [frontend_dev, backend_dev]
166
257
 
@@ -170,47 +261,59 @@ module ClaudeSwarm
170
261
  # description: "Frontend developer specializing in React and modern web technologies"
171
262
  # directory: ./frontend
172
263
  # model: sonnet
173
- # 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
174
266
  # allowed_tools: [Read, Edit, Write, "Bash(npm:*)", "Bash(yarn:*)", "Bash(pnpm:*)"]
175
267
 
176
268
  # backend_dev:
177
- # description: "Backend developer focusing on APIs, databases, and server architecture"
269
+ # description: |
270
+ # Backend developer focusing on APIs, databases, and server architecture
178
271
  # directory: ../other-app/backend
179
272
  # model: sonnet
180
- # prompt: "You specialize in backend development, APIs, databases, and server architecture"
273
+ # prompt: |
274
+ # You specialize in backend development, APIs, databases, and server architecture
181
275
  # allowed_tools: [Read, Edit, Write, Bash]
182
276
 
183
277
  # devops_engineer:
184
278
  # description: "DevOps engineer managing infrastructure, CI/CD, and deployments"
185
279
  # directory: .
186
280
  # model: sonnet
187
- # prompt: "You specialize in infrastructure, CI/CD, containerization, and deployment"
281
+ # prompt: |
282
+ # You specialize in infrastrujcture, CI/CD, containerization, and deployment
188
283
  # allowed_tools: [Read, Edit, Write, "Bash(docker:*)", "Bash(kubectl:*)", "Bash(terraform:*)"]
189
284
 
190
285
  # qa_engineer:
191
286
  # description: "QA engineer ensuring quality through comprehensive testing"
192
287
  # directory: ./tests
193
288
  # model: sonnet
194
- # prompt: "You specialize in testing, quality assurance, and test automation"
289
+ # prompt: |
290
+ # You specialize in testing, quality assurance, and test automation
195
291
  # allowed_tools: [Read, Edit, Write, Bash]
196
292
  YAML
197
293
 
198
294
  File.write(config_path, template)
199
- say "Created #{config_path}", :green
200
- 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")
201
297
  end
202
298
 
203
299
  desc "generate", "Launch Claude to help generate a swarm configuration interactively"
204
- method_option :output, aliases: "-o", type: :string,
205
- desc: "Output file path for the generated configuration"
206
- method_option :model, aliases: "-m", type: :string, default: "sonnet",
207
- 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"
208
309
  def generate
209
310
  # Check if claude command exists
210
- unless system("which claude > /dev/null 2>&1")
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
311
+ begin
312
+ system!("command -v claude > /dev/null 2>&1")
313
+ rescue Error
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,92 +337,87 @@ 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"
240
344
  def ps
241
- require_relative "commands/ps"
242
345
  Commands::Ps.new.execute
243
346
  end
244
347
 
245
348
  desc "show SESSION_ID", "Show detailed session information"
246
349
  def show(session_id)
247
- require_relative "commands/show"
248
350
  Commands::Show.new.execute(session_id)
249
351
  end
250
352
 
251
- desc "clean", "Remove stale session symlinks"
252
- method_option :days, aliases: "-d", type: :numeric, default: 7,
253
- desc: "Remove sessions older than N days"
353
+ desc "clean", "Remove stale session symlinks and orphaned worktrees"
354
+ method_option :days,
355
+ aliases: "-d",
356
+ type: :numeric,
357
+ default: 7,
358
+ desc: "Remove sessions older than N days"
254
359
  def clean
255
- run_dir = File.expand_path("~/.claude-swarm/run")
256
- unless Dir.exist?(run_dir)
257
- say "No run directory found", :yellow
258
- return
259
- end
360
+ # Clean stale symlinks
361
+ cleaned_symlinks = clean_stale_symlinks(options[:days])
260
362
 
261
- cleaned = 0
262
- Dir.glob("#{run_dir}/*").each do |symlink|
263
- next unless File.symlink?(symlink)
363
+ # Clean orphaned worktrees
364
+ cleaned_worktrees = clean_orphaned_worktrees(options[:days])
264
365
 
265
- begin
266
- # Remove if target doesn't exist (stale)
267
- unless File.exist?(File.readlink(symlink))
268
- File.unlink(symlink)
269
- cleaned += 1
270
- next
271
- end
272
-
273
- # Remove if older than specified days
274
- if File.stat(symlink).mtime < Time.now - (options[:days] * 86_400)
275
- File.unlink(symlink)
276
- cleaned += 1
277
- end
278
- rescue StandardError
279
- # Skip problematic symlinks
280
- end
366
+ if cleaned_symlinks.positive? || cleaned_worktrees.positive?
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)
369
+ else
370
+ say("No cleanup needed", :green)
281
371
  end
372
+ end
282
373
 
283
- say "Cleaned #{cleaned} stale session#{"s" unless cleaned == 1}", :green
374
+ desc "restore SESSION_ID", "Restore a previous session by ID"
375
+ def restore(session_id)
376
+ restore_session(session_id)
284
377
  end
285
378
 
286
379
  desc "watch SESSION_ID", "Watch session logs"
287
- method_option :lines, aliases: "-n", type: :numeric, default: 100,
288
- 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"
289
385
  def watch(session_id)
290
386
  # Find session path
291
387
  run_symlink = File.join(File.expand_path("~/.claude-swarm/run"), session_id)
292
388
  session_path = if File.symlink?(run_symlink)
293
- File.readlink(run_symlink)
294
- else
295
- # Search in sessions directory
296
- Dir.glob(File.expand_path("~/.claude-swarm/sessions/*/*")).find do |path|
297
- File.basename(path) == session_id
298
- end
299
- 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
300
396
 
301
397
  unless session_path && Dir.exist?(session_path)
302
- error "Session not found: #{session_id}"
303
- exit 1
398
+ error("Session not found: #{session_id}")
399
+ exit(1)
304
400
  end
305
401
 
306
402
  log_file = File.join(session_path, "session.log")
307
403
  unless File.exist?(log_file)
308
- error "Log file not found for session: #{session_id}"
309
- exit 1
404
+ error("Log file not found for session: #{session_id}")
405
+ exit(1)
310
406
  end
311
407
 
312
408
  exec("tail", "-f", "-n", options[:lines].to_s, log_file)
313
409
  end
314
410
 
315
411
  desc "list-sessions", "List all available Claude Swarm sessions"
316
- method_option :limit, aliases: "-l", type: :numeric, default: 10,
317
- 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"
318
417
  def list_sessions
319
418
  sessions_dir = File.expand_path("~/.claude-swarm/sessions")
320
419
  unless Dir.exist?(sessions_dir)
321
- say "No sessions found", :yellow
420
+ say("No sessions found", :yellow)
322
421
  return
323
422
  end
324
423
 
@@ -354,7 +453,7 @@ module ClaudeSwarm
354
453
  main_instance: main_instance,
355
454
  instances_count: mcp_files.size,
356
455
  swarm_name: swarm_name,
357
- config_path: config_file
456
+ config_path: config_file,
358
457
  }
359
458
  rescue StandardError
360
459
  # Skip invalid manifests
@@ -362,7 +461,7 @@ module ClaudeSwarm
362
461
  end
363
462
 
364
463
  if sessions.empty?
365
- say "No sessions found", :yellow
464
+ say("No sessions found", :yellow)
366
465
  return
367
466
  end
368
467
 
@@ -371,18 +470,18 @@ module ClaudeSwarm
371
470
  sessions = sessions.first(options[:limit])
372
471
 
373
472
  # Display sessions
374
- say "\nAvailable sessions (newest first):\n", :bold
473
+ say("\nAvailable sessions (newest first):\n", :bold)
375
474
  sessions.each do |session|
376
- say "\n#{session[:project]}/#{session[:id]}", :green
377
- say " Created: #{session[:created_at].strftime("%Y-%m-%d %H:%M:%S")}"
378
- say " Main: #{session[:main_instance]}"
379
- say " Instances: #{session[:instances_count]}"
380
- say " Swarm: #{session[:swarm_name]}"
381
- 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)
382
481
  end
383
482
 
384
- say "\nTo resume a session, run:", :bold
385
- say " claude-swarm --session-id <session-id>", :cyan
483
+ say("\nTo resume a session, run:", :bold)
484
+ say(" claude-swarm restore <session-id>", :cyan)
386
485
  end
387
486
 
388
487
  default_task :start
@@ -390,33 +489,33 @@ module ClaudeSwarm
390
489
  private
391
490
 
392
491
  def error(message)
393
- say message, :red
492
+ say(message, :red)
394
493
  end
395
494
 
396
495
  def restore_session(session_id)
397
- say "Restoring session: #{session_id}", :green
496
+ say("Restoring session: #{session_id}", :green)
398
497
 
399
498
  # Find the session path
400
499
  session_path = find_session_path(session_id)
401
500
  unless session_path
402
- error "Session not found: #{session_id}"
403
- exit 1
501
+ error("Session not found: #{session_id}")
502
+ exit(1)
404
503
  end
405
504
 
406
505
  begin
407
506
  # Load session info from instance ID in MCP config
408
507
  mcp_files = Dir.glob(File.join(session_path, "*.mcp.json"))
409
508
  if mcp_files.empty?
410
- error "No MCP configuration files found in session"
411
- exit 1
509
+ error("No MCP configuration files found in session")
510
+ exit(1)
412
511
  end
413
512
 
414
513
  # Load the configuration from the session directory
415
514
  config_file = File.join(session_path, "config.yml")
416
515
 
417
516
  unless File.exist?(config_file)
418
- error "Configuration file not found in session"
419
- exit 1
517
+ error("Configuration file not found in session")
518
+ exit(1)
420
519
  end
421
520
 
422
521
  # Change to the original start directory if it exists
@@ -425,10 +524,10 @@ module ClaudeSwarm
425
524
  original_dir = File.read(start_dir_file).strip
426
525
  if Dir.exist?(original_dir)
427
526
  Dir.chdir(original_dir)
428
- say "Changed to original directory: #{original_dir}", :green unless options[:prompt]
527
+ say("Changed to original directory: #{original_dir}", :green) unless options[:prompt]
429
528
  else
430
- error "Original directory no longer exists: #{original_dir}"
431
- exit 1
529
+ error("Original directory no longer exists: #{original_dir}")
530
+ exit(1)
432
531
  end
433
532
  end
434
533
 
@@ -441,33 +540,34 @@ module ClaudeSwarm
441
540
  metadata = JSON.parse(File.read(session_metadata_file))
442
541
  if metadata["worktree"] && metadata["worktree"]["enabled"]
443
542
  worktree_name = metadata["worktree"]["name"]
444
- say "Restoring with worktree: #{worktree_name}", :green unless options[:prompt]
543
+ say("Restoring with worktree: #{worktree_name}", :green) unless options[:prompt]
445
544
  end
446
545
  end
447
546
 
448
547
  # Create orchestrator with restoration mode
449
548
  generator = McpGenerator.new(config, vibe: options[:vibe], restore_session_path: session_path)
450
- orchestrator = Orchestrator.new(config, generator,
451
- vibe: options[:vibe],
452
- prompt: options[:prompt],
453
- stream_logs: options[:stream_logs],
454
- debug: options[:debug],
455
- restore_session_path: session_path,
456
- 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
+ )
457
560
  orchestrator.start
458
561
  rescue StandardError => e
459
- error "Failed to restore session: #{e.message}"
460
- error e.backtrace.join("\n") if options[:debug]
461
- exit 1
562
+ error("Failed to restore session: #{e.message}")
563
+ error(e.backtrace.join("\n")) if options[:debug]
564
+ exit(1)
462
565
  end
463
566
  end
464
567
 
465
568
  def find_session_path(session_id)
466
569
  sessions_dir = File.expand_path("~/.claude-swarm/sessions")
467
570
 
468
- # Check if it's a full path
469
- return session_id if File.exist?(File.join(session_id, "config.yml"))
470
-
471
571
  # Search for the session ID in all projects
472
572
  Dir.glob("#{sessions_dir}/*/#{session_id}").each do |path|
473
573
  config_path = File.join(path, "config.yml")
@@ -477,81 +577,100 @@ module ClaudeSwarm
477
577
  nil
478
578
  end
479
579
 
580
+ def clean_stale_symlinks(days)
581
+ run_dir = File.expand_path("~/.claude-swarm/run")
582
+ return 0 unless Dir.exist?(run_dir)
583
+
584
+ cleaned = 0
585
+ Dir.glob("#{run_dir}/*").each do |symlink|
586
+ next unless File.symlink?(symlink)
587
+
588
+ begin
589
+ # Remove if target doesn't exist (stale)
590
+ unless File.exist?(File.readlink(symlink))
591
+ File.unlink(symlink)
592
+ cleaned += 1
593
+ next
594
+ end
595
+
596
+ # Remove if older than specified days
597
+ if File.stat(symlink).mtime < Time.now - (days * 86_400)
598
+ File.unlink(symlink)
599
+ cleaned += 1
600
+ end
601
+ rescue StandardError
602
+ # Skip problematic symlinks
603
+ end
604
+ end
605
+
606
+ cleaned
607
+ end
608
+
609
+ def clean_orphaned_worktrees(days)
610
+ worktrees_dir = File.expand_path("~/.claude-swarm/worktrees")
611
+ return 0 unless Dir.exist?(worktrees_dir)
612
+
613
+ sessions_dir = File.expand_path("~/.claude-swarm/sessions")
614
+ cleaned = 0
615
+
616
+ Dir.glob("#{worktrees_dir}/*").each do |session_worktree_dir|
617
+ session_id = File.basename(session_worktree_dir)
618
+
619
+ # Skip if session still exists
620
+ next if Dir.glob("#{sessions_dir}/*/#{session_id}").any? { |path| File.exist?(File.join(path, "config.yml")) }
621
+
622
+ # Check age of worktree directory
623
+ begin
624
+ if File.stat(session_worktree_dir).mtime < Time.now - (days * 86_400)
625
+ # Remove all git worktrees in this session directory
626
+ Dir.glob("#{session_worktree_dir}/*/*").each do |worktree_path|
627
+ next unless File.directory?(worktree_path)
628
+
629
+ # Try to find the git repo and remove the worktree properly
630
+ git_dir = File.join(worktree_path, ".git")
631
+ if File.exist?(git_dir)
632
+ # Read the gitdir file to find the repo
633
+ gitdir_content = File.read(git_dir).strip
634
+ if gitdir_content.start_with?("gitdir:")
635
+ repo_git_path = gitdir_content.sub("gitdir: ", "")
636
+ # Extract repo path from .git/worktrees path
637
+ repo_path = repo_git_path.split("/.git/worktrees/").first
638
+
639
+ # Try to remove worktree via git
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
+ )
651
+ end
652
+ end
653
+
654
+ # Force remove directory if it still exists
655
+ FileUtils.rm_rf(worktree_path)
656
+ end
657
+
658
+ # Remove the session worktree directory
659
+ FileUtils.rm_rf(session_worktree_dir)
660
+ cleaned += 1
661
+ end
662
+ rescue StandardError => e
663
+ say("Warning: Failed to clean worktree directory #{session_worktree_dir}: #{e.message}", :yellow) if options[:debug]
664
+ end
665
+ end
666
+
667
+ cleaned
668
+ end
669
+
480
670
  def build_generation_prompt(readme_content, output_file)
481
- <<~PROMPT
482
- You are a Claude Swarm configuration generator assistant. Your role is to help the user create a well-structured claude-swarm.yml file through an interactive conversation.
483
-
484
- ## Claude Swarm Overview
485
- Claude Swarm is a Ruby gem that orchestrates multiple Claude Code instances as a collaborative AI development team. It enables running AI agents with specialized roles, tools, and directory contexts, communicating via MCP (Model Context Protocol).
486
-
487
- Key capabilities:
488
- - Define multiple AI instances with different roles and specializations
489
- - Set up connections between instances for collaboration
490
- - Restrict tools based on each instance's responsibilities
491
- - Run instances in different directories or Git worktrees
492
- - Support for custom system prompts per instance
493
- - Choose appropriate models (opus for complex tasks, sonnet for simpler ones)
494
-
495
- ## Your Task
496
- 1. Start by asking about the user's project structure and development needs
497
- 2. Understand what kind of team they need (roles, specializations)
498
- 3. Suggest an appropriate swarm topology based on their needs
499
- 4. Help them refine and customize the configuration
500
- 5. Generate the final claude-swarm.yml content
501
- 6. When the configuration is complete, save it to: #{output_file || "a descriptive filename based on the swarm's function"}
502
-
503
- ## File Naming Convention
504
- #{output_file ? "The user has specified the output file: #{output_file}" : "Since no output file was specified, name the file based on the swarm's function. Examples:\n - web-dev-swarm.yml for full-stack web development teams\n - data-pipeline-swarm.yml for data processing teams\n - microservices-swarm.yml for microservice architectures\n - mobile-app-swarm.yml for mobile development teams\n - ml-research-swarm.yml for machine learning teams\n - devops-swarm.yml for infrastructure and deployment teams\n Use descriptive names that clearly indicate the swarm's purpose."}
505
-
506
- ## Configuration Structure
507
- ```yaml
508
- version: 1
509
- swarm:
510
- name: "Descriptive Swarm Name"
511
- main: main_instance_name
512
- instances:
513
- instance_name:
514
- description: "Clear description of role and responsibilities"
515
- directory: ./path/to/directory
516
- model: sonnet # or opus for complex tasks
517
- prompt: "Custom system prompt for specialization"
518
- allowed_tools: [Read, Edit, Write, Bash]
519
- connections: [other_instance_names] # Optional
520
- ```
521
-
522
- ## Best Practices to Follow
523
- - Use descriptive, role-based instance names (e.g., frontend_dev, api_architect)
524
- - Write clear descriptions explaining each instance's responsibilities
525
- - Choose opus model for complex architectural or algorithmic tasks and routine development.
526
- - Choose sonnet model for simpler tasks
527
- - Set up logical connections (e.g., lead → team members, architect → implementers), but avoid circular dependencies.
528
- - Always add this to the end of every prompt: `For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.`
529
- - Select tools based on each instance's actual needs:
530
- - Read: For code review and analysis roles
531
- - Edit: For active development roles
532
- - Write: For creating new files
533
- - Bash: For running commands, tests, builds
534
- - MultiEdit: For editing multiple files at once
535
- - WebFetch: For fetching information from the web
536
- - WebSearch: For searching the web
537
- - Use custom prompts to specialize each instance's expertise
538
- - Organize directories to match project structure
539
-
540
- ## Interactive Questions to Ask
541
- - What type of project are you working on?
542
- - What's your project's directory structure?
543
- - What are the main technologies/frameworks you're using?
544
- - What development tasks do you need help with?
545
- - Do you need specialized roles (testing, DevOps, documentation)?
546
- - Are there specific areas that need focused attention?
547
- - Do you have multiple repositories or services to coordinate?
548
-
549
- <full_readme>
550
- #{readme_content}
551
- </full_readme>
552
-
553
- Start the conversation by greeting the user and asking: "What kind of project would you like to create a Claude Swarm for?"
554
- PROMPT
671
+ template_path = File.expand_path("templates/generation_prompt.md.erb", __dir__)
672
+ template = File.read(template_path)
673
+ ERB.new(template, trim_mode: "-").result(binding)
555
674
  end
556
675
  end
557
676
  end