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,460 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'json'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'colorize'
|
7
|
+
|
8
|
+
module EnhanceSwarm
|
9
|
+
class AgentCommunicator
|
10
|
+
include Singleton
|
11
|
+
|
12
|
+
COMMUNICATION_DIR = '.enhance_swarm/communication'
|
13
|
+
MESSAGE_FILE_PATTERN = 'agent_*.json'
|
14
|
+
USER_RESPONSE_FILE = 'user_responses.json'
|
15
|
+
PROMPT_TIMEOUT = 120 # 2 minutes for user response
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@communication_dir = File.join(Dir.pwd, COMMUNICATION_DIR)
|
19
|
+
@user_responses = {}
|
20
|
+
@pending_messages = {}
|
21
|
+
@monitoring_active = false
|
22
|
+
@monitoring_thread = nil
|
23
|
+
ensure_communication_directory
|
24
|
+
load_existing_responses
|
25
|
+
end
|
26
|
+
|
27
|
+
# Agent sends a message/question to user
|
28
|
+
def agent_message(agent_id, message_type, content, options = {})
|
29
|
+
message = {
|
30
|
+
id: generate_message_id(agent_id),
|
31
|
+
agent_id: agent_id,
|
32
|
+
role: options[:role] || extract_role_from_id(agent_id),
|
33
|
+
type: message_type,
|
34
|
+
content: content,
|
35
|
+
timestamp: Time.now.iso8601,
|
36
|
+
priority: options[:priority] || :medium,
|
37
|
+
requires_response: options[:requires_response] || false,
|
38
|
+
timeout: options[:timeout] || PROMPT_TIMEOUT,
|
39
|
+
quick_actions: options[:quick_actions] || [],
|
40
|
+
context: options[:context] || {}
|
41
|
+
}
|
42
|
+
|
43
|
+
save_message(message)
|
44
|
+
|
45
|
+
if message[:requires_response]
|
46
|
+
@pending_messages[message[:id]] = message
|
47
|
+
prompt_user_for_response(message) if options[:immediate_prompt]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Notify user through notification system
|
51
|
+
notify_user_of_message(message)
|
52
|
+
|
53
|
+
message[:id]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Agent asks a quick question requiring user response
|
57
|
+
def agent_question(agent_id, question, quick_actions = [], options = {})
|
58
|
+
agent_message(
|
59
|
+
agent_id,
|
60
|
+
:question,
|
61
|
+
question,
|
62
|
+
{
|
63
|
+
requires_response: true,
|
64
|
+
quick_actions: quick_actions,
|
65
|
+
immediate_prompt: options[:immediate_prompt] || false,
|
66
|
+
priority: options[:priority] || :high,
|
67
|
+
**options
|
68
|
+
}
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Agent provides status update
|
73
|
+
def agent_status(agent_id, status, details = {})
|
74
|
+
agent_message(
|
75
|
+
agent_id,
|
76
|
+
:status,
|
77
|
+
status,
|
78
|
+
{
|
79
|
+
requires_response: false,
|
80
|
+
priority: :low,
|
81
|
+
context: details
|
82
|
+
}
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Agent reports progress
|
87
|
+
def agent_progress(agent_id, progress_message, percentage = nil, eta = nil)
|
88
|
+
agent_message(
|
89
|
+
agent_id,
|
90
|
+
:progress,
|
91
|
+
progress_message,
|
92
|
+
{
|
93
|
+
requires_response: false,
|
94
|
+
priority: :low,
|
95
|
+
context: {
|
96
|
+
percentage: percentage,
|
97
|
+
eta: eta&.iso8601
|
98
|
+
}
|
99
|
+
}
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Agent requests user decision
|
104
|
+
def agent_decision(agent_id, decision_prompt, options_list, default = nil)
|
105
|
+
agent_message(
|
106
|
+
agent_id,
|
107
|
+
:decision,
|
108
|
+
decision_prompt,
|
109
|
+
{
|
110
|
+
requires_response: true,
|
111
|
+
quick_actions: options_list,
|
112
|
+
immediate_prompt: true,
|
113
|
+
priority: :high,
|
114
|
+
context: { default: default }
|
115
|
+
}
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
# User responds to a pending message
|
120
|
+
def user_respond(message_id, response)
|
121
|
+
if @pending_messages[message_id]
|
122
|
+
@user_responses[message_id] = {
|
123
|
+
response: response,
|
124
|
+
timestamp: Time.now.iso8601
|
125
|
+
}
|
126
|
+
|
127
|
+
save_user_responses
|
128
|
+
@pending_messages.delete(message_id)
|
129
|
+
|
130
|
+
# Notify agent via file system
|
131
|
+
create_response_file(message_id, response)
|
132
|
+
|
133
|
+
puts "ā
Response sent to agent".colorize(:green)
|
134
|
+
true
|
135
|
+
else
|
136
|
+
puts "ā Message ID not found or already responded".colorize(:red)
|
137
|
+
false
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Get pending messages for user
|
142
|
+
def pending_messages
|
143
|
+
@pending_messages.values.sort_by { |msg| msg[:timestamp] }
|
144
|
+
end
|
145
|
+
|
146
|
+
# Get recent messages (responded + pending)
|
147
|
+
def recent_messages(limit = 10)
|
148
|
+
all_messages = load_all_messages
|
149
|
+
all_messages.sort_by { |msg| msg[:timestamp] }.last(limit)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Check for agent response to user input
|
153
|
+
def agent_get_response(message_id, timeout = PROMPT_TIMEOUT)
|
154
|
+
start_time = Time.now
|
155
|
+
|
156
|
+
while Time.now - start_time < timeout
|
157
|
+
response_file = File.join(@communication_dir, "response_#{message_id}.json")
|
158
|
+
|
159
|
+
if File.exist?(response_file)
|
160
|
+
response_data = JSON.parse(File.read(response_file))
|
161
|
+
File.delete(response_file) # Cleanup
|
162
|
+
return response_data['response']
|
163
|
+
end
|
164
|
+
|
165
|
+
sleep(1)
|
166
|
+
end
|
167
|
+
|
168
|
+
nil # Timeout
|
169
|
+
end
|
170
|
+
|
171
|
+
# Start monitoring for user responses (CLI integration)
|
172
|
+
def start_monitoring
|
173
|
+
return if @monitoring_active
|
174
|
+
|
175
|
+
@monitoring_active = true
|
176
|
+
@monitoring_thread = Thread.new do
|
177
|
+
monitor_for_pending_messages
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def stop_monitoring
|
182
|
+
@monitoring_active = false
|
183
|
+
@monitoring_thread&.kill
|
184
|
+
@monitoring_thread = nil
|
185
|
+
end
|
186
|
+
|
187
|
+
# CLI: Show pending messages
|
188
|
+
def show_pending_messages
|
189
|
+
pending = pending_messages
|
190
|
+
|
191
|
+
if pending.empty?
|
192
|
+
puts "No pending messages from agents".colorize(:yellow)
|
193
|
+
return
|
194
|
+
end
|
195
|
+
|
196
|
+
puts "\nš¬ Pending Agent Messages:".colorize(:blue)
|
197
|
+
pending.each_with_index do |message, index|
|
198
|
+
show_message_summary(message, index + 1)
|
199
|
+
end
|
200
|
+
|
201
|
+
puts "\nUse 'enhance-swarm communicate --respond <id> <response>' to reply".colorize(:light_black)
|
202
|
+
end
|
203
|
+
|
204
|
+
# CLI: Interactive response mode
|
205
|
+
def interactive_response_mode
|
206
|
+
pending = pending_messages
|
207
|
+
|
208
|
+
if pending.empty?
|
209
|
+
puts "No pending messages".colorize(:yellow)
|
210
|
+
return
|
211
|
+
end
|
212
|
+
|
213
|
+
puts "\nš¬ Interactive Agent Communication".colorize(:blue)
|
214
|
+
|
215
|
+
pending.each_with_index do |message, index|
|
216
|
+
puts "\n#{'-' * 60}".colorize(:light_black)
|
217
|
+
show_message_detail(message, index + 1)
|
218
|
+
|
219
|
+
if message[:quick_actions].any?
|
220
|
+
puts "\nQuick actions:".colorize(:yellow)
|
221
|
+
message[:quick_actions].each_with_index do |action, i|
|
222
|
+
puts " #{i + 1}. #{action}"
|
223
|
+
end
|
224
|
+
puts " c. Custom response"
|
225
|
+
end
|
226
|
+
|
227
|
+
print "\nYour response: ".colorize(:blue)
|
228
|
+
response = $stdin.gets&.chomp
|
229
|
+
|
230
|
+
next if response.nil? || response.empty?
|
231
|
+
|
232
|
+
# Handle quick action selection
|
233
|
+
if message[:quick_actions].any? && response.match?(/^\d+$/)
|
234
|
+
action_index = response.to_i - 1
|
235
|
+
if action_index >= 0 && action_index < message[:quick_actions].length
|
236
|
+
response = message[:quick_actions][action_index]
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
user_respond(message[:id], response)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Clean up old messages
|
245
|
+
def cleanup_old_messages(days_old = 7)
|
246
|
+
cutoff = Time.now - (days_old * 24 * 60 * 60)
|
247
|
+
|
248
|
+
Dir.glob(File.join(@communication_dir, MESSAGE_FILE_PATTERN)).each do |file|
|
249
|
+
begin
|
250
|
+
message = JSON.parse(File.read(file))
|
251
|
+
message_time = Time.parse(message['timestamp'])
|
252
|
+
|
253
|
+
if message_time < cutoff
|
254
|
+
File.delete(file)
|
255
|
+
end
|
256
|
+
rescue
|
257
|
+
# Delete malformed files
|
258
|
+
File.delete(file)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
private
|
264
|
+
|
265
|
+
def ensure_communication_directory
|
266
|
+
FileUtils.mkdir_p(@communication_dir) unless Dir.exist?(@communication_dir)
|
267
|
+
end
|
268
|
+
|
269
|
+
def generate_message_id(agent_id)
|
270
|
+
"#{agent_id}_#{Time.now.to_i}_#{rand(1000)}"
|
271
|
+
end
|
272
|
+
|
273
|
+
def extract_role_from_id(agent_id)
|
274
|
+
agent_id.split('-').first
|
275
|
+
end
|
276
|
+
|
277
|
+
def save_message(message)
|
278
|
+
filename = "agent_#{message[:id]}.json"
|
279
|
+
filepath = File.join(@communication_dir, filename)
|
280
|
+
|
281
|
+
File.write(filepath, JSON.pretty_generate(message))
|
282
|
+
end
|
283
|
+
|
284
|
+
def load_all_messages
|
285
|
+
messages = []
|
286
|
+
|
287
|
+
Dir.glob(File.join(@communication_dir, MESSAGE_FILE_PATTERN)).each do |file|
|
288
|
+
begin
|
289
|
+
message = JSON.parse(File.read(file), symbolize_names: true)
|
290
|
+
messages << message
|
291
|
+
rescue
|
292
|
+
# Skip malformed files
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
messages
|
297
|
+
end
|
298
|
+
|
299
|
+
def load_existing_responses
|
300
|
+
response_file = File.join(@communication_dir, USER_RESPONSE_FILE)
|
301
|
+
|
302
|
+
if File.exist?(response_file)
|
303
|
+
@user_responses = JSON.parse(File.read(response_file))
|
304
|
+
end
|
305
|
+
rescue
|
306
|
+
@user_responses = {}
|
307
|
+
end
|
308
|
+
|
309
|
+
def save_user_responses
|
310
|
+
response_file = File.join(@communication_dir, USER_RESPONSE_FILE)
|
311
|
+
File.write(response_file, JSON.pretty_generate(@user_responses))
|
312
|
+
end
|
313
|
+
|
314
|
+
def create_response_file(message_id, response)
|
315
|
+
response_file = File.join(@communication_dir, "response_#{message_id}.json")
|
316
|
+
File.write(response_file, JSON.pretty_generate({
|
317
|
+
message_id: message_id,
|
318
|
+
response: response,
|
319
|
+
timestamp: Time.now.iso8601
|
320
|
+
}))
|
321
|
+
end
|
322
|
+
|
323
|
+
def notify_user_of_message(message)
|
324
|
+
return unless defined?(NotificationManager)
|
325
|
+
|
326
|
+
notification_content = case message[:type]
|
327
|
+
when :question
|
328
|
+
"ā #{message[:role].capitalize} agent has a question"
|
329
|
+
when :decision
|
330
|
+
"š¤ #{message[:role].capitalize} agent needs a decision"
|
331
|
+
when :status
|
332
|
+
"š #{message[:role].capitalize}: #{message[:content]}"
|
333
|
+
when :progress
|
334
|
+
"š #{message[:role].capitalize}: #{message[:content]}"
|
335
|
+
else
|
336
|
+
"š¬ Message from #{message[:role]} agent"
|
337
|
+
end
|
338
|
+
|
339
|
+
priority = message[:requires_response] ? :high : :low
|
340
|
+
|
341
|
+
NotificationManager.instance.notify(
|
342
|
+
:agent_communication,
|
343
|
+
notification_content,
|
344
|
+
{
|
345
|
+
agent_id: message[:agent_id],
|
346
|
+
message_id: message[:id],
|
347
|
+
requires_response: message[:requires_response],
|
348
|
+
type: message[:type]
|
349
|
+
}
|
350
|
+
)
|
351
|
+
end
|
352
|
+
|
353
|
+
def prompt_user_for_response(message)
|
354
|
+
puts "\nš¬ Agent Message [#{message[:agent_id]}]:".colorize(:blue)
|
355
|
+
puts "#{message[:content]}"
|
356
|
+
|
357
|
+
if message[:quick_actions].any?
|
358
|
+
puts "\nQuick actions:".colorize(:yellow)
|
359
|
+
message[:quick_actions].each_with_index do |action, i|
|
360
|
+
puts " #{i + 1}. #{action}"
|
361
|
+
end
|
362
|
+
puts " Or provide custom response:"
|
363
|
+
end
|
364
|
+
|
365
|
+
puts "Use 'enhance-swarm communicate --respond #{message[:id]} <response>' to reply".colorize(:light_black)
|
366
|
+
end
|
367
|
+
|
368
|
+
def monitor_for_pending_messages
|
369
|
+
while @monitoring_active
|
370
|
+
# Check for messages that need immediate user attention
|
371
|
+
@pending_messages.values.each do |message|
|
372
|
+
age = Time.now - Time.parse(message[:timestamp])
|
373
|
+
|
374
|
+
# Prompt if message is getting old and high priority
|
375
|
+
if age > 60 && message[:priority] == :high && !message[:notified]
|
376
|
+
puts "\nā ļø Urgent: Agent #{message[:agent_id]} waiting for response!".colorize(:red)
|
377
|
+
puts "Message: #{message[:content]}"
|
378
|
+
puts "Use 'enhance-swarm communicate --list' to see all pending messages".colorize(:light_black)
|
379
|
+
|
380
|
+
message[:notified] = true
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
sleep(30) # Check every 30 seconds
|
385
|
+
end
|
386
|
+
rescue StandardError => e
|
387
|
+
Logger.error("Communication monitoring error: #{e.message}")
|
388
|
+
end
|
389
|
+
|
390
|
+
def show_message_summary(message, index)
|
391
|
+
age = time_ago_in_words(Time.parse(message[:timestamp]))
|
392
|
+
priority_color = case message[:priority]
|
393
|
+
when :high then :yellow
|
394
|
+
when :critical then :red
|
395
|
+
else :white
|
396
|
+
end
|
397
|
+
|
398
|
+
puts "#{index}. [#{message[:id]}] #{message[:type].upcase} from #{message[:role]} (#{age} ago)".colorize(priority_color)
|
399
|
+
puts " #{message[:content][0..80]}#{message[:content].length > 80 ? '...' : ''}"
|
400
|
+
end
|
401
|
+
|
402
|
+
def show_message_detail(message, index)
|
403
|
+
age = time_ago_in_words(Time.parse(message[:timestamp]))
|
404
|
+
|
405
|
+
puts "#{index}. Message from #{message[:role]} agent [#{message[:agent_id]}]".colorize(:blue)
|
406
|
+
puts " Type: #{message[:type]}".colorize(:light_black)
|
407
|
+
puts " Priority: #{message[:priority]}".colorize(:light_black)
|
408
|
+
puts " Sent: #{age} ago".colorize(:light_black)
|
409
|
+
puts "\n#{message[:content]}"
|
410
|
+
|
411
|
+
if message[:context] && message[:context].any?
|
412
|
+
puts "\nContext:".colorize(:light_black)
|
413
|
+
message[:context].each do |key, value|
|
414
|
+
puts " #{key}: #{value}"
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
def time_ago_in_words(time)
|
420
|
+
seconds = Time.now - time
|
421
|
+
|
422
|
+
if seconds < 60
|
423
|
+
"#{seconds.round}s"
|
424
|
+
elsif seconds < 3600
|
425
|
+
"#{(seconds / 60).round}m"
|
426
|
+
elsif seconds < 86400
|
427
|
+
"#{(seconds / 3600).round}h"
|
428
|
+
else
|
429
|
+
"#{(seconds / 86400).round}d"
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
# Class methods for singleton access
|
434
|
+
class << self
|
435
|
+
def instance
|
436
|
+
@instance ||= new
|
437
|
+
end
|
438
|
+
|
439
|
+
def agent_message(*args)
|
440
|
+
instance.agent_message(*args)
|
441
|
+
end
|
442
|
+
|
443
|
+
def agent_question(*args)
|
444
|
+
instance.agent_question(*args)
|
445
|
+
end
|
446
|
+
|
447
|
+
def agent_status(*args)
|
448
|
+
instance.agent_status(*args)
|
449
|
+
end
|
450
|
+
|
451
|
+
def agent_progress(*args)
|
452
|
+
instance.agent_progress(*args)
|
453
|
+
end
|
454
|
+
|
455
|
+
def agent_decision(*args)
|
456
|
+
instance.agent_decision(*args)
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|