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.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -63
- data/.rubocop_todo.yml +11 -0
- data/CHANGELOG.md +110 -0
- data/CLAUDE.md +64 -2
- data/README.md +190 -28
- data/Rakefile +1 -1
- data/examples/mixed-provider-swarm.yml +23 -0
- data/examples/monitoring-demo.yml +4 -4
- data/lib/claude_swarm/claude_code_executor.rb +7 -13
- data/lib/claude_swarm/claude_mcp_server.rb +26 -17
- data/lib/claude_swarm/cli.rb +384 -265
- data/lib/claude_swarm/commands/ps.rb +22 -24
- data/lib/claude_swarm/commands/show.rb +45 -63
- data/lib/claude_swarm/configuration.rb +137 -8
- data/lib/claude_swarm/mcp_generator.rb +39 -15
- data/lib/claude_swarm/openai/chat_completion.rb +264 -0
- data/lib/claude_swarm/openai/executor.rb +301 -0
- data/lib/claude_swarm/openai/responses.rb +338 -0
- data/lib/claude_swarm/orchestrator.rb +221 -45
- data/lib/claude_swarm/process_tracker.rb +7 -7
- data/lib/claude_swarm/session_cost_calculator.rb +93 -0
- data/lib/claude_swarm/session_path.rb +3 -5
- data/lib/claude_swarm/system_utils.rb +16 -0
- data/lib/claude_swarm/templates/generation_prompt.md.erb +230 -0
- data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
- data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
- data/lib/claude_swarm/tools/task_tool.rb +43 -0
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +145 -48
- data/lib/claude_swarm.rb +34 -12
- data/llms.txt +2 -2
- data/single.yml +482 -6
- data/team.yml +344 -0
- metadata +65 -14
- data/claude-swarm.yml +0 -64
- data/lib/claude_swarm/reset_session_tool.rb +0 -22
- data/lib/claude_swarm/session_info_tool.rb +0 -22
- data/lib/claude_swarm/task_tool.rb +0 -39
- /data/{example → examples}/claude-swarm.yml +0 -0
- /data/{example → examples}/microservices-team.yml +0 -0
- /data/{example → examples}/session-restoration-demo.yml +0 -0
- /data/{example → examples}/test-generation.yml +0 -0
data/lib/claude_swarm/cli.rb
CHANGED
@@ -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
|
-
|
13
|
-
|
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 :
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
method_option :prompt,
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
method_option :
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
method_option :
|
30
|
-
|
31
|
-
|
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
|
-
|
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
|
42
|
-
exit
|
40
|
+
error("Configuration file not found: #{config_path}")
|
41
|
+
exit(1)
|
43
42
|
end
|
44
43
|
|
45
|
-
say
|
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
|
50
|
-
exit
|
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(
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
65
|
-
exit
|
67
|
+
error(e.message)
|
68
|
+
exit(1)
|
66
69
|
rescue StandardError => e
|
67
|
-
error
|
68
|
-
error
|
69
|
-
exit
|
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,
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
method_option :
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
method_option :
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
method_option :
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
132
|
-
error
|
133
|
-
exit
|
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,
|
139
|
-
|
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
|
145
|
-
error
|
146
|
-
exit
|
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:
|
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:
|
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:
|
269
|
+
# description: |
|
270
|
+
# Backend developer focusing on APIs, databases, and server architecture
|
178
271
|
# directory: ../other-app/backend
|
179
272
|
# model: sonnet
|
180
|
-
# prompt:
|
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:
|
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:
|
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
|
200
|
-
say
|
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,
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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",
|
227
|
-
|
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
|
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,
|
253
|
-
|
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
|
-
|
256
|
-
|
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
|
-
|
262
|
-
|
263
|
-
next unless File.symlink?(symlink)
|
363
|
+
# Clean orphaned worktrees
|
364
|
+
cleaned_worktrees = clean_orphaned_worktrees(options[:days])
|
264
365
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
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
|
-
|
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,
|
288
|
-
|
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
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
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
|
303
|
-
exit
|
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
|
309
|
-
exit
|
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,
|
317
|
-
|
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
|
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
|
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
|
473
|
+
say("\nAvailable sessions (newest first):\n", :bold)
|
375
474
|
sessions.each do |session|
|
376
|
-
say
|
377
|
-
say
|
378
|
-
say
|
379
|
-
say
|
380
|
-
say
|
381
|
-
say
|
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
|
385
|
-
say
|
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
|
492
|
+
say(message, :red)
|
394
493
|
end
|
395
494
|
|
396
495
|
def restore_session(session_id)
|
397
|
-
say
|
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
|
403
|
-
exit
|
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
|
411
|
-
exit
|
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
|
419
|
-
exit
|
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
|
527
|
+
say("Changed to original directory: #{original_dir}", :green) unless options[:prompt]
|
429
528
|
else
|
430
|
-
error
|
431
|
-
exit
|
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
|
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(
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
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
|
460
|
-
error
|
461
|
-
exit
|
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
|
-
|
482
|
-
|
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
|