enhance_swarm 1.0.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 +7 -0
- data/.enhance_swarm/agent_scripts/frontend_agent.md +39 -0
- data/.enhance_swarm/user_patterns.json +37 -0
- data/CHANGELOG.md +184 -0
- data/LICENSE +21 -0
- data/PRODUCTION_TEST_LOG.md +502 -0
- data/README.md +905 -0
- data/Rakefile +28 -0
- data/USAGE_EXAMPLES.md +477 -0
- data/examples/enhance_workflow.md +346 -0
- data/examples/rails_project.md +253 -0
- data/exe/enhance-swarm +30 -0
- data/lib/enhance_swarm/additional_commands.rb +299 -0
- data/lib/enhance_swarm/agent_communicator.rb +460 -0
- data/lib/enhance_swarm/agent_reviewer.rb +283 -0
- data/lib/enhance_swarm/agent_spawner.rb +462 -0
- data/lib/enhance_swarm/cleanup_manager.rb +245 -0
- data/lib/enhance_swarm/cli.rb +1592 -0
- data/lib/enhance_swarm/command_executor.rb +78 -0
- data/lib/enhance_swarm/configuration.rb +324 -0
- data/lib/enhance_swarm/control_agent.rb +307 -0
- data/lib/enhance_swarm/dependency_validator.rb +195 -0
- data/lib/enhance_swarm/error_recovery.rb +785 -0
- data/lib/enhance_swarm/generator.rb +194 -0
- data/lib/enhance_swarm/interrupt_handler.rb +512 -0
- data/lib/enhance_swarm/logger.rb +106 -0
- data/lib/enhance_swarm/mcp_integration.rb +85 -0
- data/lib/enhance_swarm/monitor.rb +28 -0
- data/lib/enhance_swarm/notification_manager.rb +444 -0
- data/lib/enhance_swarm/orchestrator.rb +313 -0
- data/lib/enhance_swarm/output_streamer.rb +281 -0
- data/lib/enhance_swarm/process_monitor.rb +266 -0
- data/lib/enhance_swarm/progress_tracker.rb +215 -0
- data/lib/enhance_swarm/project_analyzer.rb +612 -0
- data/lib/enhance_swarm/resource_manager.rb +177 -0
- data/lib/enhance_swarm/retry_handler.rb +40 -0
- data/lib/enhance_swarm/session_manager.rb +247 -0
- data/lib/enhance_swarm/signal_handler.rb +95 -0
- data/lib/enhance_swarm/smart_defaults.rb +708 -0
- data/lib/enhance_swarm/task_integration.rb +150 -0
- data/lib/enhance_swarm/task_manager.rb +174 -0
- data/lib/enhance_swarm/version.rb +5 -0
- data/lib/enhance_swarm/visual_dashboard.rb +555 -0
- data/lib/enhance_swarm/web_ui.rb +211 -0
- data/lib/enhance_swarm.rb +69 -0
- data/setup.sh +86 -0
- data/sig/enhance_swarm.rbs +4 -0
- data/templates/claude/CLAUDE.md +160 -0
- data/templates/claude/MCP.md +117 -0
- data/templates/claude/PERSONAS.md +114 -0
- data/templates/claude/RULES.md +221 -0
- data/test_builtin_functionality.rb +121 -0
- data/test_core_components.rb +156 -0
- data/test_real_claude_integration.rb +285 -0
- data/test_security.rb +150 -0
- data/test_smart_defaults.rb +155 -0
- data/test_task_integration.rb +173 -0
- data/test_web_ui.rb +245 -0
- data/web/assets/css/main.css +645 -0
- data/web/assets/js/kanban.js +499 -0
- data/web/assets/js/main.js +525 -0
- data/web/templates/dashboard.html.erb +226 -0
- data/web/templates/kanban.html.erb +193 -0
- metadata +293 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'shellwords'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
module EnhanceSwarm
|
8
|
+
class CommandExecutor
|
9
|
+
class CommandError < StandardError
|
10
|
+
attr_reader :exit_status, :stderr
|
11
|
+
|
12
|
+
def initialize(message, exit_status: nil, stderr: nil)
|
13
|
+
super(message)
|
14
|
+
@exit_status = exit_status
|
15
|
+
@stderr = stderr
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.execute(command, *args, timeout: 30, input: nil)
|
20
|
+
# Sanitize command and arguments
|
21
|
+
safe_command = sanitize_command(command)
|
22
|
+
safe_args = args.map { |arg| sanitize_argument(arg) }
|
23
|
+
|
24
|
+
begin
|
25
|
+
Timeout.timeout(timeout) do
|
26
|
+
stdout, stderr, status = Open3.capture3(safe_command, *safe_args,
|
27
|
+
stdin_data: input)
|
28
|
+
|
29
|
+
unless status.success?
|
30
|
+
raise CommandError.new(
|
31
|
+
"Command failed: #{safe_command} #{safe_args.join(' ')}",
|
32
|
+
exit_status: status.exitstatus,
|
33
|
+
stderr: stderr
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
stdout.strip
|
38
|
+
end
|
39
|
+
rescue Timeout::Error
|
40
|
+
raise CommandError.new("Command timed out after #{timeout} seconds")
|
41
|
+
rescue Errno::ENOENT
|
42
|
+
raise CommandError.new("Command not found: #{safe_command}")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.execute_async(command, *args)
|
47
|
+
safe_command = sanitize_command(command)
|
48
|
+
safe_args = args.map { |arg| sanitize_argument(arg) }
|
49
|
+
|
50
|
+
begin
|
51
|
+
pid = Process.spawn(safe_command, *safe_args)
|
52
|
+
Process.detach(pid)
|
53
|
+
pid
|
54
|
+
rescue Errno::ENOENT
|
55
|
+
raise CommandError.new("Command not found: #{safe_command}")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.command_available?(command)
|
60
|
+
execute('which', command, timeout: 5)
|
61
|
+
true
|
62
|
+
rescue CommandError
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.sanitize_command(command)
|
67
|
+
# Only allow alphanumeric, dash, underscore, and slash
|
68
|
+
raise ArgumentError, "Invalid command: #{command}" unless command.match?(%r{\A[a-zA-Z0-9_\-/]+\z})
|
69
|
+
|
70
|
+
command
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.sanitize_argument(arg)
|
74
|
+
# Convert to string and escape shell metacharacters
|
75
|
+
Shellwords.escape(arg.to_s)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,324 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'psych'
|
4
|
+
require_relative 'project_analyzer'
|
5
|
+
|
6
|
+
module EnhanceSwarm
|
7
|
+
class Configuration
|
8
|
+
attr_accessor :project_name, :project_description, :technology_stack,
|
9
|
+
:test_command, :task_command, :task_move_command,
|
10
|
+
:code_standards, :important_notes,
|
11
|
+
:max_concurrent_agents, :monitor_interval,
|
12
|
+
:monitor_timeout, :worktree_enabled,
|
13
|
+
:mcp_tools, :gemini_enabled, :desktop_commander_enabled,
|
14
|
+
:max_memory_mb, :max_disk_mb
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
# Apply smart defaults from project analysis if no config exists
|
18
|
+
if config_file_exists?
|
19
|
+
apply_hardcoded_defaults
|
20
|
+
load_from_file
|
21
|
+
else
|
22
|
+
apply_smart_defaults
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_h
|
27
|
+
{
|
28
|
+
project: {
|
29
|
+
name: @project_name,
|
30
|
+
description: @project_description,
|
31
|
+
technology_stack: @technology_stack
|
32
|
+
},
|
33
|
+
commands: {
|
34
|
+
test: @test_command,
|
35
|
+
task: @task_command,
|
36
|
+
task_move: @task_move_command
|
37
|
+
},
|
38
|
+
orchestration: {
|
39
|
+
max_concurrent_agents: @max_concurrent_agents,
|
40
|
+
monitor_interval: @monitor_interval,
|
41
|
+
monitor_timeout: @monitor_timeout,
|
42
|
+
worktree_enabled: @worktree_enabled
|
43
|
+
},
|
44
|
+
mcp: {
|
45
|
+
tools: @mcp_tools,
|
46
|
+
gemini_enabled: @gemini_enabled,
|
47
|
+
desktop_commander_enabled: @desktop_commander_enabled
|
48
|
+
},
|
49
|
+
standards: {
|
50
|
+
code: @code_standards,
|
51
|
+
notes: @important_notes
|
52
|
+
}
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def save!
|
57
|
+
File.write(config_file_path, Psych.dump(to_h))
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def apply_hardcoded_defaults
|
63
|
+
# Project defaults
|
64
|
+
@project_name = 'Project'
|
65
|
+
@project_description = 'A project managed by EnhanceSwarm'
|
66
|
+
@technology_stack = 'Ruby on Rails'
|
67
|
+
|
68
|
+
# Commands
|
69
|
+
@test_command = 'bundle exec rails test'
|
70
|
+
@task_command = 'bundle exec swarm-tasks'
|
71
|
+
@task_move_command = 'bundle exec swarm-tasks move'
|
72
|
+
|
73
|
+
# Standards
|
74
|
+
@code_standards = default_code_standards
|
75
|
+
@important_notes = []
|
76
|
+
|
77
|
+
# Orchestration settings
|
78
|
+
@max_concurrent_agents = 4
|
79
|
+
@monitor_interval = 30
|
80
|
+
@monitor_timeout = 120 # 2 minutes max per monitoring session
|
81
|
+
@worktree_enabled = true
|
82
|
+
|
83
|
+
# Resource limits
|
84
|
+
@max_memory_mb = 2048 # 2GB max memory for all agents
|
85
|
+
@max_disk_mb = 1024 # 1GB max disk usage
|
86
|
+
|
87
|
+
# MCP settings
|
88
|
+
@mcp_tools = {
|
89
|
+
context7: true,
|
90
|
+
sequential: true,
|
91
|
+
magic_ui: true,
|
92
|
+
puppeteer: true
|
93
|
+
}
|
94
|
+
@gemini_enabled = true
|
95
|
+
@desktop_commander_enabled = true
|
96
|
+
end
|
97
|
+
|
98
|
+
def apply_smart_defaults
|
99
|
+
Logger.info("No configuration file found, applying smart defaults based on project analysis")
|
100
|
+
|
101
|
+
# Analyze the current project
|
102
|
+
analyzer = ProjectAnalyzer.new
|
103
|
+
analyzer.analyze
|
104
|
+
smart_defaults = analyzer.generate_smart_defaults
|
105
|
+
|
106
|
+
# Apply smart defaults with fallbacks
|
107
|
+
@project_name = smart_defaults[:project_name] || File.basename(Dir.pwd)
|
108
|
+
@project_description = smart_defaults[:project_description] || 'A project managed by EnhanceSwarm'
|
109
|
+
@technology_stack = smart_defaults[:technology_stack]&.join(', ') || 'Multiple Technologies'
|
110
|
+
|
111
|
+
# Smart command detection
|
112
|
+
@test_command = smart_defaults[:test_command] || detect_test_command_fallback
|
113
|
+
@task_command = 'bundle exec swarm-tasks'
|
114
|
+
@task_move_command = 'bundle exec swarm-tasks move'
|
115
|
+
|
116
|
+
# Code standards based on project type
|
117
|
+
@code_standards = generate_smart_code_standards(analyzer.analysis_results[:project_type])
|
118
|
+
@important_notes = generate_smart_notes(analyzer.analysis_results)
|
119
|
+
|
120
|
+
# Orchestration settings based on project size
|
121
|
+
@max_concurrent_agents = smart_defaults[:max_concurrent_agents] || 3
|
122
|
+
@monitor_interval = 30
|
123
|
+
@monitor_timeout = 120
|
124
|
+
@worktree_enabled = true
|
125
|
+
|
126
|
+
# MCP settings - enable all by default for smart detection
|
127
|
+
@mcp_tools = {
|
128
|
+
context7: true,
|
129
|
+
sequential: true,
|
130
|
+
magic_ui: analyzer.analysis_results[:frontend_framework] ? true : false,
|
131
|
+
puppeteer: analyzer.analysis_results[:testing_framework]&.any? { |f| f.include?('Cypress') || f.include?('Playwright') } || false
|
132
|
+
}
|
133
|
+
@gemini_enabled = true
|
134
|
+
@desktop_commander_enabled = smart_defaults[:has_documentation] || false
|
135
|
+
|
136
|
+
Logger.info("Applied smart defaults for #{@project_name} (#{@technology_stack})")
|
137
|
+
end
|
138
|
+
|
139
|
+
def detect_test_command_fallback
|
140
|
+
return 'bundle exec rspec' if File.exist?('spec')
|
141
|
+
return 'bundle exec rails test' if File.exist?('test')
|
142
|
+
return 'npm test' if File.exist?('package.json')
|
143
|
+
return 'pytest' if File.exist?('pytest.ini') || File.exist?('conftest.py')
|
144
|
+
|
145
|
+
'echo "No test command configured"'
|
146
|
+
end
|
147
|
+
|
148
|
+
def generate_smart_code_standards(project_type)
|
149
|
+
base_standards = [
|
150
|
+
'Follow framework conventions',
|
151
|
+
'Write tests for all new features',
|
152
|
+
'Use clear, descriptive naming'
|
153
|
+
]
|
154
|
+
|
155
|
+
case project_type
|
156
|
+
when 'rails'
|
157
|
+
base_standards + [
|
158
|
+
'Use service objects for business logic',
|
159
|
+
'Keep controllers thin',
|
160
|
+
'Use strong validations in models'
|
161
|
+
]
|
162
|
+
when 'react', 'vue', 'angular', 'nextjs'
|
163
|
+
base_standards + [
|
164
|
+
'Use functional components with hooks',
|
165
|
+
'Implement proper state management',
|
166
|
+
'Optimize for performance and accessibility'
|
167
|
+
]
|
168
|
+
when 'django', 'flask'
|
169
|
+
base_standards + [
|
170
|
+
'Follow MVC patterns',
|
171
|
+
'Use proper authentication and authorization',
|
172
|
+
'Implement comprehensive API documentation'
|
173
|
+
]
|
174
|
+
else
|
175
|
+
base_standards + [
|
176
|
+
'Maintain consistent code style',
|
177
|
+
'Document complex logic',
|
178
|
+
'Use version control best practices'
|
179
|
+
]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def generate_smart_notes(analysis_results)
|
184
|
+
notes = []
|
185
|
+
|
186
|
+
if analysis_results[:documentation]&.dig(:has_docs)
|
187
|
+
notes << "Project has documentation in #{analysis_results[:documentation][:primary_path]} - consider this context for changes"
|
188
|
+
end
|
189
|
+
|
190
|
+
if analysis_results[:testing_framework]&.any?
|
191
|
+
frameworks = analysis_results[:testing_framework].join(', ')
|
192
|
+
notes << "Testing framework(s) detected: #{frameworks} - ensure new features include tests"
|
193
|
+
end
|
194
|
+
|
195
|
+
if analysis_results[:deployment]&.any?
|
196
|
+
deployments = analysis_results[:deployment].join(', ')
|
197
|
+
notes << "Deployment configurations found: #{deployments} - consider deployment impact for changes"
|
198
|
+
end
|
199
|
+
|
200
|
+
if analysis_results[:database]&.any?
|
201
|
+
databases = analysis_results[:database].join(', ')
|
202
|
+
notes << "Database(s) in use: #{databases} - consider data migration needs"
|
203
|
+
end
|
204
|
+
|
205
|
+
notes
|
206
|
+
end
|
207
|
+
|
208
|
+
def config_file_path
|
209
|
+
File.join(EnhanceSwarm.root, '.enhance_swarm.yml')
|
210
|
+
end
|
211
|
+
|
212
|
+
def config_file_exists?
|
213
|
+
File.exist?(config_file_path)
|
214
|
+
end
|
215
|
+
|
216
|
+
def load_from_file
|
217
|
+
config = Psych.safe_load(File.read(config_file_path), permitted_classes: [Symbol])
|
218
|
+
|
219
|
+
# Project settings with validation
|
220
|
+
@project_name = validate_string(config.dig('project', 'name')) || @project_name
|
221
|
+
@project_description = validate_string(config.dig('project', 'description')) || @project_description
|
222
|
+
@technology_stack = validate_string(config.dig('project', 'technology_stack')) || @technology_stack
|
223
|
+
|
224
|
+
# Commands with validation
|
225
|
+
@test_command = validate_command(config.dig('commands', 'test')) || @test_command
|
226
|
+
@task_command = validate_command(config.dig('commands', 'task')) || @task_command
|
227
|
+
@task_move_command = validate_command(config.dig('commands', 'task_move')) || @task_move_command
|
228
|
+
|
229
|
+
# Orchestration with validation
|
230
|
+
@max_concurrent_agents = validate_positive_integer(config.dig('orchestration',
|
231
|
+
'max_concurrent_agents')) || @max_concurrent_agents
|
232
|
+
@monitor_interval = validate_positive_integer(config.dig('orchestration',
|
233
|
+
'monitor_interval')) || @monitor_interval
|
234
|
+
@monitor_timeout = validate_positive_integer(config.dig('orchestration', 'monitor_timeout')) || @monitor_timeout
|
235
|
+
@worktree_enabled = config.dig('orchestration', 'worktree_enabled') != false
|
236
|
+
|
237
|
+
# MCP
|
238
|
+
@mcp_tools = validate_hash(config.dig('mcp', 'tools')) || @mcp_tools
|
239
|
+
@gemini_enabled = config.dig('mcp', 'gemini_enabled') != false
|
240
|
+
@desktop_commander_enabled = config.dig('mcp', 'desktop_commander_enabled') != false
|
241
|
+
|
242
|
+
# Standards
|
243
|
+
@code_standards = validate_array(config.dig('standards', 'code')) || @code_standards
|
244
|
+
@important_notes = validate_array(config.dig('standards', 'notes')) || @important_notes
|
245
|
+
rescue Psych::SyntaxError => e
|
246
|
+
puts "Configuration file has invalid YAML syntax: #{e.message}"
|
247
|
+
# Use defaults
|
248
|
+
rescue StandardError => e
|
249
|
+
puts "Error loading configuration: #{e.message}"
|
250
|
+
# Use defaults
|
251
|
+
end
|
252
|
+
|
253
|
+
def default_code_standards
|
254
|
+
[
|
255
|
+
'Follow framework conventions',
|
256
|
+
'Use service objects for business logic',
|
257
|
+
'Keep controllers thin',
|
258
|
+
'Write tests for all new features',
|
259
|
+
'Use strong validations in models'
|
260
|
+
]
|
261
|
+
end
|
262
|
+
|
263
|
+
# Validation methods
|
264
|
+
def validate_string(value)
|
265
|
+
return nil unless value.is_a?(String)
|
266
|
+
|
267
|
+
# Remove dangerous characters
|
268
|
+
sanitized = value.gsub(/[`$\\;|&]/, '').strip
|
269
|
+
sanitized.empty? ? nil : sanitized
|
270
|
+
end
|
271
|
+
|
272
|
+
def validate_command(value)
|
273
|
+
return nil unless value.is_a?(String)
|
274
|
+
|
275
|
+
# Only allow safe command characters
|
276
|
+
sanitized = value.gsub(/[;|&`$\\]/, '').strip
|
277
|
+
return nil if sanitized.empty?
|
278
|
+
|
279
|
+
# Check for common dangerous patterns but allow 'exec' in normal contexts
|
280
|
+
dangerous_patterns = [/\brm\s+-rf/, /\|\s*sh\b/, /\beval\s*\(/, /\bexec\s*\(/]
|
281
|
+
dangerous_patterns.each do |pattern|
|
282
|
+
return nil if sanitized.match?(pattern)
|
283
|
+
end
|
284
|
+
|
285
|
+
sanitized
|
286
|
+
end
|
287
|
+
|
288
|
+
def validate_positive_integer(value)
|
289
|
+
return nil unless value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A\d+\z/))
|
290
|
+
|
291
|
+
int_value = value.to_i
|
292
|
+
int_value.positive? ? int_value : nil
|
293
|
+
end
|
294
|
+
|
295
|
+
def validate_array(value)
|
296
|
+
return nil unless value.is_a?(Array)
|
297
|
+
|
298
|
+
# Validate each element as a string
|
299
|
+
validated = value.map { |item| validate_string(item) }.compact
|
300
|
+
validated.empty? ? nil : validated
|
301
|
+
end
|
302
|
+
|
303
|
+
def validate_hash(value)
|
304
|
+
return nil unless value.is_a?(Hash)
|
305
|
+
|
306
|
+
# Ensure all keys are strings or symbols and values are safe
|
307
|
+
validated = {}
|
308
|
+
value.each do |k, v|
|
309
|
+
next unless k.is_a?(String) || k.is_a?(Symbol)
|
310
|
+
|
311
|
+
validated[k] = case v
|
312
|
+
when String
|
313
|
+
validate_string(v)
|
314
|
+
when TrueClass, FalseClass
|
315
|
+
v
|
316
|
+
when Integer
|
317
|
+
validate_positive_integer(v)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
validated.empty? ? nil : validated
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
@@ -0,0 +1,307 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'tempfile'
|
5
|
+
|
6
|
+
module EnhanceSwarm
|
7
|
+
class ControlAgent
|
8
|
+
attr_reader :task, :config, :status, :worker_agents, :start_time
|
9
|
+
|
10
|
+
def initialize(task_description, config = nil)
|
11
|
+
@task = task_description
|
12
|
+
@config = config || EnhanceSwarm.configuration
|
13
|
+
@status = 'initializing'
|
14
|
+
@worker_agents = {}
|
15
|
+
@start_time = Time.now
|
16
|
+
@communication_file = create_communication_file
|
17
|
+
@control_process = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def start_coordination
|
21
|
+
Logger.info("Starting Control Agent for task: #{@task}")
|
22
|
+
@status = 'starting'
|
23
|
+
|
24
|
+
# Spawn the Control Agent as a Claude process
|
25
|
+
spawn_control_agent
|
26
|
+
|
27
|
+
# Monitor and coordinate
|
28
|
+
coordinate_agents
|
29
|
+
end
|
30
|
+
|
31
|
+
def stop_coordination
|
32
|
+
Logger.info("Stopping Control Agent coordination")
|
33
|
+
@status = 'stopping'
|
34
|
+
|
35
|
+
# Terminate control agent process
|
36
|
+
if @control_process
|
37
|
+
begin
|
38
|
+
Process.kill('TERM', @control_process)
|
39
|
+
Process.wait(@control_process)
|
40
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
41
|
+
# Process already terminated
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Cleanup
|
46
|
+
cleanup_resources
|
47
|
+
end
|
48
|
+
|
49
|
+
def current_status
|
50
|
+
return default_status unless File.exist?(@communication_file)
|
51
|
+
|
52
|
+
begin
|
53
|
+
content = File.read(@communication_file)
|
54
|
+
return default_status if content.strip.empty?
|
55
|
+
|
56
|
+
# Parse the latest status update from Control Agent
|
57
|
+
lines = content.split("\n").reject(&:empty?)
|
58
|
+
latest_status = lines.last
|
59
|
+
|
60
|
+
JSON.parse(latest_status)
|
61
|
+
rescue JSON::ParserError, StandardError => e
|
62
|
+
Logger.warn("Failed to parse control agent status: #{e.message}")
|
63
|
+
default_status
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def worker_agent_summary
|
68
|
+
status = current_status
|
69
|
+
{
|
70
|
+
total: status['active_agents'].length + status['completed_agents'].length,
|
71
|
+
active: status['active_agents'].length,
|
72
|
+
completed: status['completed_agents'].length,
|
73
|
+
progress: status['progress_percentage'] || 0,
|
74
|
+
phase: status['phase'] || 'unknown',
|
75
|
+
estimated_completion: status['estimated_completion']
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def spawn_control_agent
|
82
|
+
prompt = build_control_agent_prompt
|
83
|
+
|
84
|
+
# Create a temporary prompt file
|
85
|
+
prompt_file = Tempfile.new(['control_prompt', '.txt'])
|
86
|
+
prompt_file.write(prompt)
|
87
|
+
prompt_file.close
|
88
|
+
|
89
|
+
begin
|
90
|
+
# Spawn Control Agent using claude command
|
91
|
+
@control_process = RetryHandler.with_retry(max_retries: 2) do
|
92
|
+
CommandExecutor.execute_async(
|
93
|
+
'claude',
|
94
|
+
'--role=control',
|
95
|
+
'--file', prompt_file.path,
|
96
|
+
'--output', @communication_file,
|
97
|
+
'--continuous'
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
Logger.info("Control Agent spawned with PID: #{@control_process}")
|
102
|
+
@status = 'coordinating'
|
103
|
+
|
104
|
+
rescue RetryHandler::RetryError, CommandExecutor::CommandError => e
|
105
|
+
Logger.error("Failed to spawn Control Agent: #{e.message}")
|
106
|
+
@status = 'failed'
|
107
|
+
raise
|
108
|
+
ensure
|
109
|
+
prompt_file.unlink
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def coordinate_agents
|
114
|
+
coordination_thread = Thread.new do
|
115
|
+
while @status == 'coordinating'
|
116
|
+
begin
|
117
|
+
# Read latest status from Control Agent
|
118
|
+
agent_status = current_status
|
119
|
+
|
120
|
+
# Update our internal state
|
121
|
+
update_worker_agents(agent_status)
|
122
|
+
|
123
|
+
# Check if coordination is complete
|
124
|
+
if agent_status['status'] == 'completed'
|
125
|
+
@status = 'completed'
|
126
|
+
break
|
127
|
+
elsif agent_status['status'] == 'failed'
|
128
|
+
@status = 'failed'
|
129
|
+
break
|
130
|
+
end
|
131
|
+
|
132
|
+
sleep(5) # Check every 5 seconds
|
133
|
+
|
134
|
+
rescue StandardError => e
|
135
|
+
Logger.error("Control Agent coordination error: #{e.message}")
|
136
|
+
sleep(10) # Back off on errors
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
Logger.info("Control Agent coordination finished with status: #{@status}")
|
141
|
+
end
|
142
|
+
|
143
|
+
coordination_thread
|
144
|
+
end
|
145
|
+
|
146
|
+
def update_worker_agents(agent_status)
|
147
|
+
# Track active agents
|
148
|
+
agent_status['active_agents']&.each do |agent_id|
|
149
|
+
unless @worker_agents[agent_id]
|
150
|
+
@worker_agents[agent_id] = {
|
151
|
+
id: agent_id,
|
152
|
+
status: 'active',
|
153
|
+
start_time: Time.now
|
154
|
+
}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Track completed agents
|
159
|
+
agent_status['completed_agents']&.each do |agent_id|
|
160
|
+
if @worker_agents[agent_id]
|
161
|
+
@worker_agents[agent_id][:status] = 'completed'
|
162
|
+
@worker_agents[agent_id][:completion_time] = Time.now
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def build_control_agent_prompt
|
168
|
+
<<~PROMPT
|
169
|
+
AUTONOMOUS CONTROL AGENT - MULTI-AGENT COORDINATION
|
170
|
+
|
171
|
+
TASK: #{@task}
|
172
|
+
|
173
|
+
YOUR ROLE: You are a Control Agent responsible for coordinating multiple worker agents to complete this task efficiently. You have full autonomous authority to make decisions and spawn agents.
|
174
|
+
|
175
|
+
CAPABILITIES:
|
176
|
+
1. Spawn worker agents using: claude-swarm start --role=<role> -p "<specific_task>"
|
177
|
+
2. Monitor progress via git commands: git status, git log, git diff
|
178
|
+
3. Analyze file changes and commits to understand progress
|
179
|
+
4. Make handoff decisions based on dependencies and completion status
|
180
|
+
5. Coordinate timing to prevent conflicts
|
181
|
+
|
182
|
+
WORKER AGENT ROLES:
|
183
|
+
- backend: Models, APIs, database, business logic, migrations, services
|
184
|
+
- frontend: Controllers, views, JavaScript, CSS, user interface, forms
|
185
|
+
- ux: User experience design, templates, layouts, styling, wireframes
|
186
|
+
- qa: Testing, specs, edge cases, quality assurance, validation
|
187
|
+
|
188
|
+
COORDINATION STRATEGY:
|
189
|
+
1. Analyze the task to determine which roles are needed
|
190
|
+
2. Identify dependencies (typically: backend → frontend → qa)
|
191
|
+
3. Spawn agents in dependency order
|
192
|
+
4. Monitor their progress via git commits and file changes
|
193
|
+
5. Signal next agent when prerequisites are met
|
194
|
+
6. Handle conflicts and coordination issues
|
195
|
+
|
196
|
+
COMMUNICATION PROTOCOL:
|
197
|
+
You MUST output status updates in JSON format to #{@communication_file}
|
198
|
+
Update every 30 seconds with current status.
|
199
|
+
|
200
|
+
Required JSON format:
|
201
|
+
{
|
202
|
+
"status": "coordinating|completed|failed",
|
203
|
+
"phase": "analysis|backend_implementation|frontend_integration|qa_validation|completion",
|
204
|
+
"active_agents": ["agent-id-1", "agent-id-2"],
|
205
|
+
"completed_agents": ["agent-id-3"],
|
206
|
+
"failed_agents": [],
|
207
|
+
"progress_percentage": 45,
|
208
|
+
"estimated_completion": "2025-06-28T20:30:00Z",
|
209
|
+
"message": "Backend agent completed auth model, starting frontend integration",
|
210
|
+
"next_actions": ["spawn_frontend_agent", "monitor_integration_conflicts"],
|
211
|
+
"dependencies_met": ["backend_models_complete"],
|
212
|
+
"blocking_issues": []
|
213
|
+
}
|
214
|
+
|
215
|
+
CRITICAL INSTRUCTIONS:
|
216
|
+
1. You have FULL PERMISSION to execute commands and spawn agents
|
217
|
+
2. Work directory: #{Dir.pwd}
|
218
|
+
3. Start immediately by analyzing the task and creating an execution plan
|
219
|
+
4. Spawn the first agent(s) based on dependencies
|
220
|
+
5. Continuously monitor and coordinate until task completion
|
221
|
+
6. Handle errors gracefully and retry failed operations
|
222
|
+
7. Ensure all agents complete successfully before marking task complete
|
223
|
+
|
224
|
+
PROJECT CONTEXT:
|
225
|
+
- Technology stack: #{Array(@config.technology_stack).join(', ')}
|
226
|
+
- Test command: #{@config.test_command}
|
227
|
+
- Project type: #{@config.project_name}
|
228
|
+
|
229
|
+
BEGIN COORDINATION NOW.
|
230
|
+
PROMPT
|
231
|
+
end
|
232
|
+
|
233
|
+
def create_communication_file
|
234
|
+
# Create a temporary file for Control Agent communication
|
235
|
+
temp_file = Tempfile.new(['control_agent_status', '.json'])
|
236
|
+
temp_file.close
|
237
|
+
temp_file.path
|
238
|
+
end
|
239
|
+
|
240
|
+
def default_status
|
241
|
+
{
|
242
|
+
'status' => @status,
|
243
|
+
'phase' => 'initializing',
|
244
|
+
'active_agents' => [],
|
245
|
+
'completed_agents' => [],
|
246
|
+
'failed_agents' => [],
|
247
|
+
'progress_percentage' => 0,
|
248
|
+
'message' => 'Control Agent initializing...',
|
249
|
+
'estimated_completion' => nil
|
250
|
+
}
|
251
|
+
end
|
252
|
+
|
253
|
+
def cleanup_resources
|
254
|
+
# Clean up communication file
|
255
|
+
File.unlink(@communication_file) if File.exist?(@communication_file)
|
256
|
+
rescue StandardError => e
|
257
|
+
Logger.warn("Failed to cleanup Control Agent resources: #{e.message}")
|
258
|
+
end
|
259
|
+
|
260
|
+
# Class methods for easy usage
|
261
|
+
def self.coordinate_task(task_description, config: nil)
|
262
|
+
control_agent = new(task_description, config)
|
263
|
+
|
264
|
+
begin
|
265
|
+
coordination_thread = control_agent.start_coordination
|
266
|
+
|
267
|
+
# Return control agent for monitoring
|
268
|
+
yield control_agent if block_given?
|
269
|
+
|
270
|
+
# Wait for coordination to complete
|
271
|
+
coordination_thread.join if coordination_thread
|
272
|
+
|
273
|
+
control_agent.current_status
|
274
|
+
ensure
|
275
|
+
control_agent.stop_coordination
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Enhanced progress tracking integration
|
280
|
+
def track_progress_with_streamer(streamer = nil)
|
281
|
+
return unless streamer
|
282
|
+
|
283
|
+
Thread.new do
|
284
|
+
while @status == 'coordinating'
|
285
|
+
status = current_status
|
286
|
+
|
287
|
+
# Update progress tracker
|
288
|
+
progress = status['progress_percentage'] || 0
|
289
|
+
message = status['message'] || 'Coordinating agents...'
|
290
|
+
|
291
|
+
streamer.set_progress(progress,
|
292
|
+
message: message,
|
293
|
+
operation: 'control_coordination',
|
294
|
+
details: {
|
295
|
+
phase: status['phase'],
|
296
|
+
active_agents: status['active_agents']&.length || 0,
|
297
|
+
completed_agents: status['completed_agents']&.length || 0
|
298
|
+
})
|
299
|
+
|
300
|
+
break if %w[completed failed].include?(status['status'])
|
301
|
+
|
302
|
+
sleep(2)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|