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,313 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'json'
|
5
|
+
require_relative 'command_executor'
|
6
|
+
require_relative 'retry_handler'
|
7
|
+
require_relative 'logger'
|
8
|
+
require_relative 'cleanup_manager'
|
9
|
+
require_relative 'session_manager'
|
10
|
+
require_relative 'agent_spawner'
|
11
|
+
require_relative 'process_monitor'
|
12
|
+
require_relative 'task_integration'
|
13
|
+
|
14
|
+
module EnhanceSwarm
|
15
|
+
class Orchestrator
|
16
|
+
def initialize
|
17
|
+
@config = EnhanceSwarm.configuration
|
18
|
+
@task_manager = TaskManager.new
|
19
|
+
@session_manager = SessionManager.new
|
20
|
+
@agent_spawner = AgentSpawner.new
|
21
|
+
@process_monitor = ProcessMonitor.new
|
22
|
+
@task_integration = TaskIntegration.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def enhance(task_id: nil, dry_run: false, follow: false)
|
26
|
+
puts '๐ฏ ENHANCE Protocol Initiated'.colorize(:green)
|
27
|
+
|
28
|
+
# Step 1: Identify task
|
29
|
+
task = task_id ? @task_manager.find_task(task_id) : @task_manager.next_priority_task
|
30
|
+
unless task
|
31
|
+
puts 'No tasks available in backlog'.colorize(:yellow)
|
32
|
+
return
|
33
|
+
end
|
34
|
+
|
35
|
+
puts "๐ Task: #{task[:id]} - #{task[:title]}".colorize(:blue)
|
36
|
+
|
37
|
+
if dry_run
|
38
|
+
puts "\n๐ Dry run - would execute:".colorize(:yellow)
|
39
|
+
show_execution_plan(task)
|
40
|
+
return
|
41
|
+
end
|
42
|
+
|
43
|
+
# Step 2: Create or resume session
|
44
|
+
unless @session_manager.session_exists?
|
45
|
+
@session_manager.create_session(task[:description])
|
46
|
+
puts '๐ Created new session'.colorize(:green)
|
47
|
+
else
|
48
|
+
puts '๐ Resuming existing session'.colorize(:blue)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Step 3: Move task to active
|
52
|
+
@task_manager.move_task(task[:id], 'active')
|
53
|
+
puts 'โ
Task moved to active'.colorize(:green)
|
54
|
+
|
55
|
+
# Step 4: Break down and spawn agents with progress tracking
|
56
|
+
agents = break_down_task(task)
|
57
|
+
total_tokens = agents.sum { |agent| ProgressTracker.estimate_tokens_for_operation('spawn_agent') }
|
58
|
+
|
59
|
+
if follow
|
60
|
+
spawn_agents_with_streaming(agents)
|
61
|
+
else
|
62
|
+
ProgressTracker.track(total_steps: 100, estimated_tokens: total_tokens) do |tracker|
|
63
|
+
# Spawn agents (0-50% progress)
|
64
|
+
spawn_result = spawn_agents(agents, tracker)
|
65
|
+
|
66
|
+
# Brief monitoring (50-100% progress)
|
67
|
+
monitor_with_progress(tracker, @config.monitor_timeout)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Step 5: Continue with other work
|
72
|
+
puts "\n๐ก Agents working in background. Check back later with:".colorize(:blue)
|
73
|
+
puts ' enhance-swarm status'
|
74
|
+
puts ' enhance-swarm monitor'
|
75
|
+
|
76
|
+
# Return control to user for other work
|
77
|
+
end
|
78
|
+
|
79
|
+
def monitor_with_progress(tracker, timeout_seconds)
|
80
|
+
start_time = Time.now
|
81
|
+
last_check = start_time
|
82
|
+
check_interval = 5 # Check every 5 seconds
|
83
|
+
|
84
|
+
while (Time.now - start_time) < timeout_seconds
|
85
|
+
elapsed = Time.now - start_time
|
86
|
+
progress = 50 + (elapsed / timeout_seconds * 50).to_i # 50-100% progress
|
87
|
+
|
88
|
+
# Get current status using built-in monitor
|
89
|
+
status = @process_monitor.status
|
90
|
+
active_count = status[:active_agents]
|
91
|
+
|
92
|
+
tracker.set_progress(progress,
|
93
|
+
message: "Monitoring #{active_count} active agents...",
|
94
|
+
operation: 'monitor',
|
95
|
+
details: {
|
96
|
+
active_agents: active_count,
|
97
|
+
elapsed: "#{elapsed.round}s"
|
98
|
+
})
|
99
|
+
|
100
|
+
sleep(check_interval)
|
101
|
+
end
|
102
|
+
|
103
|
+
tracker.set_progress(100, message: "Monitoring complete - agents running in background")
|
104
|
+
end
|
105
|
+
|
106
|
+
def spawn_agents_with_streaming(agents)
|
107
|
+
puts "\n๐ค Spawning #{agents.count} agents with live output...".colorize(:yellow)
|
108
|
+
|
109
|
+
# Use built-in agent spawner
|
110
|
+
spawned_agents = @agent_spawner.spawn_multiple_agents(agents)
|
111
|
+
|
112
|
+
return if spawned_agents.empty?
|
113
|
+
|
114
|
+
puts "\n๐ด Live output streaming started for #{spawned_agents.count} agents. Press Ctrl+C to stop watching.\n".colorize(:green)
|
115
|
+
|
116
|
+
# Start streaming output for all agents
|
117
|
+
begin
|
118
|
+
OutputStreamer.stream_agents(spawned_agents)
|
119
|
+
rescue NameError
|
120
|
+
# Fallback: Use built-in monitoring if OutputStreamer doesn't exist
|
121
|
+
puts "๐ Switching to built-in monitoring...".colorize(:blue)
|
122
|
+
@process_monitor.watch(interval: 3)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def spawn_single(task:, role:, worktree:)
|
127
|
+
operation_id = "spawn_#{role}_#{Time.now.to_i}"
|
128
|
+
Logger.log_operation(operation_id, 'started', { role: role, worktree: worktree })
|
129
|
+
|
130
|
+
puts "๐ Spawning #{role} agent...".colorize(:yellow)
|
131
|
+
|
132
|
+
begin
|
133
|
+
# Use built-in agent spawner
|
134
|
+
result = @agent_spawner.spawn_agent(
|
135
|
+
role: role,
|
136
|
+
task: task,
|
137
|
+
worktree: worktree && @config.worktree_enabled
|
138
|
+
)
|
139
|
+
|
140
|
+
if result
|
141
|
+
puts 'โ
Agent spawned successfully'.colorize(:green)
|
142
|
+
puts " ๐ PID: #{result[:pid]}, Role: #{result[:role]}".colorize(:blue)
|
143
|
+
if result[:worktree_path]
|
144
|
+
puts " ๐ Worktree: #{File.basename(result[:worktree_path])}".colorize(:blue)
|
145
|
+
end
|
146
|
+
|
147
|
+
Logger.log_operation(operation_id, 'success', { role: role, pid: result[:pid] })
|
148
|
+
|
149
|
+
# Wait a moment and check if the process is still running
|
150
|
+
sleep(1)
|
151
|
+
begin
|
152
|
+
Process.getpgid(result[:pid])
|
153
|
+
puts " ๐ Agent process confirmed running".colorize(:green)
|
154
|
+
rescue Errno::ESRCH
|
155
|
+
puts " ๐ Agent process completed quickly".colorize(:yellow)
|
156
|
+
end
|
157
|
+
|
158
|
+
result[:pid]
|
159
|
+
else
|
160
|
+
puts "โ Failed to spawn agent".colorize(:red)
|
161
|
+
puts " ๐ก Try running with ENHANCE_SWARM_DEBUG=true for details".colorize(:yellow)
|
162
|
+
Logger.log_operation(operation_id, 'failed', { role: role, error: 'Spawn failed' })
|
163
|
+
false
|
164
|
+
end
|
165
|
+
rescue StandardError => e
|
166
|
+
puts "โ Failed to spawn agent: #{e.message}".colorize(:red)
|
167
|
+
Logger.log_operation(operation_id, 'failed', { role: role, error: e.message })
|
168
|
+
false
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def get_task_management_data
|
173
|
+
@task_integration.get_kanban_data
|
174
|
+
end
|
175
|
+
|
176
|
+
def setup_task_management
|
177
|
+
@task_integration.setup_task_management
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
def break_down_task(task)
|
183
|
+
agents = []
|
184
|
+
|
185
|
+
# Analyze task description to determine needed agents
|
186
|
+
desc = task[:description].downcase
|
187
|
+
|
188
|
+
# Always include QA for any feature work
|
189
|
+
needs_qa = desc.match?(/feature|implement|add|create|build/)
|
190
|
+
|
191
|
+
# Check what types of work are needed
|
192
|
+
needs_ui = desc.match?(/ui|interface|design|template|email|view|component/)
|
193
|
+
needs_backend = desc.match?(/model|database|api|service|migration|business logic/)
|
194
|
+
needs_frontend = desc.match?(/controller|javascript|turbo|stimulus|form/)
|
195
|
+
|
196
|
+
# Build agent list based on analysis
|
197
|
+
agents << { role: 'ux', task: extract_ux_work(task) } if needs_ui
|
198
|
+
agents << { role: 'backend', task: extract_backend_work(task) } if needs_backend
|
199
|
+
agents << { role: 'frontend', task: extract_frontend_work(task) } if needs_frontend
|
200
|
+
agents << { role: 'qa', task: extract_qa_work(task) } if needs_qa
|
201
|
+
|
202
|
+
# If no specific needs detected, spawn a general agent
|
203
|
+
agents << { role: 'general', task: task[:description] } if agents.empty?
|
204
|
+
|
205
|
+
agents
|
206
|
+
end
|
207
|
+
|
208
|
+
def spawn_agents(agents, tracker = nil)
|
209
|
+
if tracker
|
210
|
+
tracker.update_status("Spawning #{agents.count} agents...", operation: 'spawn_agents')
|
211
|
+
else
|
212
|
+
puts "\n๐ค Spawning #{agents.count} agents...".colorize(:yellow)
|
213
|
+
end
|
214
|
+
|
215
|
+
spawned_agents = []
|
216
|
+
failed_agents = []
|
217
|
+
total_estimated_tokens = agents.sum { |agent| ProgressTracker.estimate_tokens_for_operation('spawn_agent') }
|
218
|
+
|
219
|
+
agents.each_with_index do |agent, index|
|
220
|
+
# Update progress if tracker available
|
221
|
+
if tracker
|
222
|
+
progress = (index.to_f / agents.count * 50).to_i # First 50% for spawning
|
223
|
+
tracker.set_progress(progress,
|
224
|
+
message: "Spawning #{agent[:role]} agent...",
|
225
|
+
agent: agent[:role],
|
226
|
+
operation: 'spawn')
|
227
|
+
end
|
228
|
+
|
229
|
+
# Add jitter to prevent resource contention
|
230
|
+
sleep(2 + rand(0..2)) if index > 0
|
231
|
+
|
232
|
+
# Use built-in agent spawner
|
233
|
+
result = @agent_spawner.spawn_agent(
|
234
|
+
role: agent[:role],
|
235
|
+
task: agent[:task],
|
236
|
+
worktree: true
|
237
|
+
)
|
238
|
+
|
239
|
+
if result
|
240
|
+
spawned_agents << { **agent, pid: result[:pid] }
|
241
|
+
tracker&.add_tokens(ProgressTracker.estimate_tokens_for_operation('spawn_agent'))
|
242
|
+
else
|
243
|
+
failed_agents << agent
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
if tracker
|
248
|
+
tracker.set_progress(50,
|
249
|
+
message: "All agents spawned, monitoring...",
|
250
|
+
operation: 'monitor')
|
251
|
+
elsif failed_agents.empty?
|
252
|
+
puts 'โ
All agents spawned'.colorize(:green)
|
253
|
+
else
|
254
|
+
puts "โ ๏ธ #{spawned_agents.size}/#{agents.size} agents spawned successfully".colorize(:yellow)
|
255
|
+
Logger.warn("Failed to spawn agents: #{failed_agents.map { |a| a[:role] }.join(', ')}")
|
256
|
+
end
|
257
|
+
|
258
|
+
{ spawned: spawned_agents, failed: failed_agents }
|
259
|
+
end
|
260
|
+
|
261
|
+
|
262
|
+
def extract_ux_work(task)
|
263
|
+
"Design and implement UI/UX for: #{task[:description]}"
|
264
|
+
end
|
265
|
+
|
266
|
+
def extract_backend_work(task)
|
267
|
+
"Implement backend logic for: #{task[:description]}"
|
268
|
+
end
|
269
|
+
|
270
|
+
def extract_frontend_work(task)
|
271
|
+
"Implement frontend integration for: #{task[:description]}"
|
272
|
+
end
|
273
|
+
|
274
|
+
def extract_qa_work(task)
|
275
|
+
"Write comprehensive tests for: #{task[:description]}"
|
276
|
+
end
|
277
|
+
|
278
|
+
def sanitize_command(command)
|
279
|
+
# Basic command sanitization - remove shell metacharacters
|
280
|
+
command.to_s.gsub(/[;&|`$\\]/, '').strip
|
281
|
+
end
|
282
|
+
|
283
|
+
def sanitize_argument(arg)
|
284
|
+
# Escape shell metacharacters in arguments
|
285
|
+
require 'shellwords'
|
286
|
+
Shellwords.escape(arg.to_s)
|
287
|
+
end
|
288
|
+
|
289
|
+
def sanitize_role(role)
|
290
|
+
# Only allow known safe roles
|
291
|
+
allowed_roles = %w[ux backend frontend qa general]
|
292
|
+
role = role.to_s.downcase.strip
|
293
|
+
allowed_roles.include?(role) ? role : 'general'
|
294
|
+
end
|
295
|
+
|
296
|
+
def show_execution_plan(task)
|
297
|
+
agents = break_down_task(task)
|
298
|
+
|
299
|
+
puts "Would spawn #{agents.count} agents:".colorize(:blue)
|
300
|
+
agents.each do |agent|
|
301
|
+
puts " - #{agent[:role].upcase}: #{agent[:task]}"
|
302
|
+
end
|
303
|
+
|
304
|
+
puts "\nActions that would be executed:"
|
305
|
+
puts " 1. Create or resume agent session"
|
306
|
+
puts " 2. #{sanitize_command(@config.task_move_command)} #{sanitize_argument(task[:id])} active"
|
307
|
+
agents.each_with_index do |agent, index|
|
308
|
+
puts " #{index + 3}. Spawn #{sanitize_role(agent[:role])} agent with git worktree"
|
309
|
+
end
|
310
|
+
puts " #{agents.count + 3}. Monitor agent progress"
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'io/console'
|
4
|
+
require 'pty'
|
5
|
+
|
6
|
+
module EnhanceSwarm
|
7
|
+
class OutputStreamer
|
8
|
+
attr_reader :agent_outputs, :active_agents
|
9
|
+
|
10
|
+
def initialize(max_lines: 10)
|
11
|
+
@max_lines = max_lines
|
12
|
+
@agent_outputs = {}
|
13
|
+
@active_agents = {}
|
14
|
+
@output_mutex = Mutex.new
|
15
|
+
@display_thread = nil
|
16
|
+
@running = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def start_streaming
|
20
|
+
@running = true
|
21
|
+
@display_thread = Thread.new { display_loop }
|
22
|
+
end
|
23
|
+
|
24
|
+
def stop_streaming
|
25
|
+
@running = false
|
26
|
+
@display_thread&.join
|
27
|
+
clear_display
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_agent(agent_id, process_pid, role: 'agent')
|
31
|
+
@output_mutex.synchronize do
|
32
|
+
@active_agents[agent_id] = {
|
33
|
+
pid: process_pid,
|
34
|
+
role: role,
|
35
|
+
start_time: Time.now,
|
36
|
+
status: 'running'
|
37
|
+
}
|
38
|
+
@agent_outputs[agent_id] = {
|
39
|
+
lines: [],
|
40
|
+
last_update: Time.now
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Start monitoring this agent's output (in real implementation)
|
45
|
+
# start_agent_monitor(agent_id, process_pid)
|
46
|
+
end
|
47
|
+
|
48
|
+
def remove_agent(agent_id, status: 'completed')
|
49
|
+
@output_mutex.synchronize do
|
50
|
+
if @active_agents[agent_id]
|
51
|
+
@active_agents[agent_id][:status] = status
|
52
|
+
@active_agents[agent_id][:end_time] = Time.now
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_output_line(agent_id, line)
|
58
|
+
@output_mutex.synchronize do
|
59
|
+
return unless @agent_outputs[agent_id]
|
60
|
+
|
61
|
+
# Clean and format the line
|
62
|
+
clean_line = clean_output_line(line)
|
63
|
+
return if clean_line.empty?
|
64
|
+
|
65
|
+
@agent_outputs[agent_id][:lines] << {
|
66
|
+
text: clean_line,
|
67
|
+
timestamp: Time.now
|
68
|
+
}
|
69
|
+
|
70
|
+
# Keep only recent lines
|
71
|
+
if @agent_outputs[agent_id][:lines].length > @max_lines
|
72
|
+
@agent_outputs[agent_id][:lines].shift
|
73
|
+
end
|
74
|
+
|
75
|
+
@agent_outputs[agent_id][:last_update] = Time.now
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def display_loop
|
82
|
+
while @running
|
83
|
+
render_display
|
84
|
+
sleep(0.1) # 10 FPS update rate
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def render_display
|
89
|
+
@output_mutex.synchronize do
|
90
|
+
clear_display
|
91
|
+
|
92
|
+
# Show active agents summary
|
93
|
+
puts build_agents_summary
|
94
|
+
puts
|
95
|
+
|
96
|
+
# Show live output for each agent
|
97
|
+
@active_agents.each do |agent_id, agent_info|
|
98
|
+
next unless agent_info[:status] == 'running'
|
99
|
+
render_agent_output(agent_id, agent_info)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Show completed agents summary
|
103
|
+
completed = @active_agents.select { |_, info| info[:status] != 'running' }
|
104
|
+
if completed.any?
|
105
|
+
puts build_completed_summary(completed)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def build_agents_summary
|
111
|
+
running = @active_agents.count { |_, info| info[:status] == 'running' }
|
112
|
+
completed = @active_agents.count { |_, info| info[:status] == 'completed' }
|
113
|
+
failed = @active_agents.count { |_, info| info[:status] == 'failed' }
|
114
|
+
|
115
|
+
summary = "๐ค Agents: #{running} running"
|
116
|
+
summary += ", #{completed} completed".colorize(:green) if completed > 0
|
117
|
+
summary += ", #{failed} failed".colorize(:red) if failed > 0
|
118
|
+
|
119
|
+
summary
|
120
|
+
end
|
121
|
+
|
122
|
+
def render_agent_output(agent_id, agent_info)
|
123
|
+
role = agent_info[:role]
|
124
|
+
elapsed = Time.now - agent_info[:start_time]
|
125
|
+
|
126
|
+
# Agent header
|
127
|
+
puts "โโ #{role_icon(role)} #{role.upcase} Agent (#{format_duration(elapsed)}) #{agent_id[0..7]} โ".ljust(60, 'โ') + 'โ'
|
128
|
+
|
129
|
+
# Recent output lines
|
130
|
+
output_data = @agent_outputs[agent_id]
|
131
|
+
if output_data[:lines].any?
|
132
|
+
output_data[:lines].last(@max_lines).each do |line_data|
|
133
|
+
age = Time.now - line_data[:timestamp]
|
134
|
+
age_indicator = age < 2 ? 'โ'.colorize(:green) : age < 10 ? 'โ'.colorize(:yellow) : 'โ'.colorize(:light_black)
|
135
|
+
puts "โ #{age_indicator} #{line_data[:text].ljust(55)[0..55]} โ"
|
136
|
+
end
|
137
|
+
else
|
138
|
+
puts "โ #{' ' * 57} โ"
|
139
|
+
puts "โ #{'No output yet...'.colorize(:light_black).ljust(57)} โ"
|
140
|
+
end
|
141
|
+
|
142
|
+
puts "โ#{'โ' * 58}โ"
|
143
|
+
puts
|
144
|
+
end
|
145
|
+
|
146
|
+
def build_completed_summary(completed_agents)
|
147
|
+
summary = "\n๐ Completed:"
|
148
|
+
completed_agents.each do |agent_id, info|
|
149
|
+
duration = info[:end_time] - info[:start_time]
|
150
|
+
status_icon = info[:status] == 'completed' ? 'โ
' : 'โ'
|
151
|
+
summary += "\n #{status_icon} #{info[:role]} (#{format_duration(duration)})"
|
152
|
+
end
|
153
|
+
summary
|
154
|
+
end
|
155
|
+
|
156
|
+
def start_agent_monitor(agent_id, process_pid)
|
157
|
+
Thread.new do
|
158
|
+
begin
|
159
|
+
# Monitor the process output by tailing its log file or stdout
|
160
|
+
# This is a simplified version - in reality you'd need to capture
|
161
|
+
# the actual agent's stdout/stderr
|
162
|
+
monitor_agent_process(agent_id, process_pid)
|
163
|
+
rescue StandardError => e
|
164
|
+
Logger.error("Failed to monitor agent #{agent_id}: #{e.message}")
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def monitor_agent_process(agent_id, process_pid)
|
170
|
+
# Check if process is still running
|
171
|
+
loop do
|
172
|
+
break unless process_running?(process_pid)
|
173
|
+
|
174
|
+
# Try to read from agent's output (this would need actual implementation)
|
175
|
+
# For now, simulate some output
|
176
|
+
if rand < 0.3 # 30% chance of output per cycle
|
177
|
+
simulate_agent_output(agent_id)
|
178
|
+
end
|
179
|
+
|
180
|
+
sleep(1)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Process ended
|
184
|
+
remove_agent(agent_id, status: 'completed')
|
185
|
+
rescue StandardError => e
|
186
|
+
remove_agent(agent_id, status: 'failed')
|
187
|
+
add_output_line(agent_id, "โ Agent failed: #{e.message}")
|
188
|
+
end
|
189
|
+
|
190
|
+
def process_running?(pid)
|
191
|
+
Process.kill(0, pid)
|
192
|
+
true
|
193
|
+
rescue Errno::ESRCH
|
194
|
+
false
|
195
|
+
rescue Errno::EPERM
|
196
|
+
true # Process exists but we can't signal it
|
197
|
+
end
|
198
|
+
|
199
|
+
def simulate_agent_output(agent_id)
|
200
|
+
# This simulates agent output - replace with actual output capture
|
201
|
+
sample_outputs = [
|
202
|
+
"๐ Analyzing project structure...",
|
203
|
+
"๐ Reading file: #{['app/models/user.rb', 'config/routes.rb', 'spec/models/user_spec.rb'].sample}",
|
204
|
+
"โ
Found existing #{['authentication', 'user model', 'routes', 'tests'].sample}",
|
205
|
+
"๐ง Generating #{['controller', 'migration', 'view', 'test'].sample}...",
|
206
|
+
"๐ Writing #{['spec/requests/auth_spec.rb', 'app/controllers/sessions_controller.rb', 'app/views/sessions/new.html.erb'].sample}",
|
207
|
+
"๐ Running #{['rspec', 'rubocop', 'tests'].sample}...",
|
208
|
+
"๐ฏ Implementing #{['login logic', 'validation', 'error handling'].sample}...",
|
209
|
+
"๐ Adding security #{['validations', 'sanitization', 'authentication'].sample}..."
|
210
|
+
]
|
211
|
+
|
212
|
+
add_output_line(agent_id, sample_outputs.sample)
|
213
|
+
end
|
214
|
+
|
215
|
+
def clean_output_line(line)
|
216
|
+
# Remove ANSI color codes and control characters
|
217
|
+
line.to_s
|
218
|
+
.gsub(/\e\[[0-9;]*m/, '') # Remove ANSI color codes
|
219
|
+
.gsub(/\r\n?/, '') # Remove carriage returns
|
220
|
+
.strip
|
221
|
+
.slice(0, 55) # Limit length
|
222
|
+
end
|
223
|
+
|
224
|
+
def role_icon(role)
|
225
|
+
icons = {
|
226
|
+
'backend' => '๐ง',
|
227
|
+
'frontend' => '๐จ',
|
228
|
+
'ux' => 'โจ',
|
229
|
+
'qa' => '๐งช',
|
230
|
+
'general' => '๐ค'
|
231
|
+
}
|
232
|
+
icons[role.to_s] || '๐ค'
|
233
|
+
end
|
234
|
+
|
235
|
+
def format_duration(seconds)
|
236
|
+
if seconds < 60
|
237
|
+
"#{seconds.round}s"
|
238
|
+
elsif seconds < 3600
|
239
|
+
"#{(seconds / 60).round}m"
|
240
|
+
else
|
241
|
+
"#{(seconds / 3600).round(1)}h"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def clear_display
|
246
|
+
# Move cursor to top and clear screen
|
247
|
+
print "\e[H\e[2J"
|
248
|
+
end
|
249
|
+
|
250
|
+
# Class methods for easy usage
|
251
|
+
def self.stream_agents(agents)
|
252
|
+
streamer = new
|
253
|
+
|
254
|
+
begin
|
255
|
+
streamer.start_streaming
|
256
|
+
|
257
|
+
# Add all agents to streamer
|
258
|
+
agents.each do |agent|
|
259
|
+
streamer.add_agent(
|
260
|
+
agent[:id] || "#{agent[:role]}-#{Time.now.to_i}",
|
261
|
+
agent[:pid],
|
262
|
+
role: agent[:role]
|
263
|
+
)
|
264
|
+
end
|
265
|
+
|
266
|
+
# Wait for user interrupt or all agents to complete
|
267
|
+
trap('INT') { streamer.stop_streaming; exit }
|
268
|
+
|
269
|
+
loop do
|
270
|
+
active_count = streamer.active_agents.count { |_, info| info[:status] == 'running' }
|
271
|
+
break if active_count == 0
|
272
|
+
|
273
|
+
sleep(1)
|
274
|
+
end
|
275
|
+
|
276
|
+
ensure
|
277
|
+
streamer.stop_streaming
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|