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,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'json'
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
module EnhanceSwarm
|
8
|
+
class Logger
|
9
|
+
LOG_LEVELS = {
|
10
|
+
debug: ::Logger::DEBUG,
|
11
|
+
info: ::Logger::INFO,
|
12
|
+
warn: ::Logger::WARN,
|
13
|
+
error: ::Logger::ERROR,
|
14
|
+
fatal: ::Logger::FATAL
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def self.logger
|
18
|
+
@logger ||= create_logger
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.create_logger
|
22
|
+
logger = ::Logger.new($stdout)
|
23
|
+
logger.level = log_level
|
24
|
+
logger.formatter = method(:format_message)
|
25
|
+
logger
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.log_level
|
29
|
+
level = ENV['ENHANCE_SWARM_LOG_LEVEL']&.downcase&.to_sym || :info
|
30
|
+
LOG_LEVELS[level] || ::Logger::INFO
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.format_message(severity, timestamp, progname, msg)
|
34
|
+
if ENV['ENHANCE_SWARM_JSON_LOGS'] == 'true'
|
35
|
+
format_json(severity, timestamp, progname, msg)
|
36
|
+
else
|
37
|
+
format_human(severity, timestamp, progname, msg)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.format_json(severity, timestamp, progname, msg)
|
42
|
+
{
|
43
|
+
timestamp: timestamp.iso8601,
|
44
|
+
level: severity,
|
45
|
+
component: progname || 'enhance_swarm',
|
46
|
+
message: msg.to_s,
|
47
|
+
pid: Process.pid
|
48
|
+
}.to_json + "\n"
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.format_human(severity, timestamp, progname, msg)
|
52
|
+
color = case severity
|
53
|
+
when 'ERROR', 'FATAL' then :red
|
54
|
+
when 'WARN' then :yellow
|
55
|
+
when 'INFO' then :blue
|
56
|
+
else :white
|
57
|
+
end
|
58
|
+
|
59
|
+
"[#{timestamp.strftime('%Y-%m-%d %H:%M:%S')}] #{severity.ljust(5)} #{msg}".colorize(color) + "\n"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Convenience methods
|
63
|
+
def self.debug(msg, component: nil)
|
64
|
+
logger.debug(msg) { component }
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.info(msg, component: nil)
|
68
|
+
logger.info(msg) { component }
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.warn(msg, component: nil)
|
72
|
+
logger.warn(msg) { component }
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.error(msg, component: nil)
|
76
|
+
logger.error(msg) { component }
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.fatal(msg, component: nil)
|
80
|
+
logger.fatal(msg) { component }
|
81
|
+
end
|
82
|
+
|
83
|
+
# Structured logging for automation
|
84
|
+
def self.log_operation(operation, status, details = {})
|
85
|
+
log_data = {
|
86
|
+
operation: operation,
|
87
|
+
status: status,
|
88
|
+
details: details,
|
89
|
+
timestamp: Time.now.iso8601
|
90
|
+
}
|
91
|
+
|
92
|
+
case status
|
93
|
+
when 'success', 'completed'
|
94
|
+
info("Operation #{operation} completed successfully", component: 'operation')
|
95
|
+
when 'failed', 'error'
|
96
|
+
error("Operation #{operation} failed: #{details[:error]}", component: 'operation')
|
97
|
+
when 'started', 'in_progress'
|
98
|
+
info("Operation #{operation} started", component: 'operation')
|
99
|
+
else
|
100
|
+
debug("Operation #{operation}: #{status}", component: 'operation')
|
101
|
+
end
|
102
|
+
|
103
|
+
log_data
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EnhanceSwarm
|
4
|
+
class MCPIntegration
|
5
|
+
def initialize
|
6
|
+
@config = EnhanceSwarm.configuration
|
7
|
+
end
|
8
|
+
|
9
|
+
def gemini_available?
|
10
|
+
@config.gemini_enabled && system('which gemini > /dev/null 2>&1')
|
11
|
+
end
|
12
|
+
|
13
|
+
def desktop_commander_available?
|
14
|
+
@config.desktop_commander_enabled
|
15
|
+
end
|
16
|
+
|
17
|
+
def analyze_with_gemini(path, prompt)
|
18
|
+
return nil unless gemini_available?
|
19
|
+
|
20
|
+
full_prompt = "@#{path} #{prompt}"
|
21
|
+
output = `gemini -p "#{full_prompt}" 2>/dev/null`
|
22
|
+
|
23
|
+
output.empty? ? nil : output
|
24
|
+
end
|
25
|
+
|
26
|
+
def setup_gemini
|
27
|
+
return if gemini_available?
|
28
|
+
|
29
|
+
puts <<~SETUP
|
30
|
+
|
31
|
+
🔧 Gemini CLI Setup Required:
|
32
|
+
|
33
|
+
1. Install Gemini CLI (if not installed)
|
34
|
+
2. Run: gemini auth login
|
35
|
+
3. Choose Google auth
|
36
|
+
4. Verify: gemini -p "test"
|
37
|
+
|
38
|
+
Gemini provides large context analysis capabilities.
|
39
|
+
SETUP
|
40
|
+
end
|
41
|
+
|
42
|
+
def setup_desktop_commander
|
43
|
+
puts <<~SETUP
|
44
|
+
|
45
|
+
🔧 Desktop Commander MCP Setup:
|
46
|
+
|
47
|
+
Desktop Commander allows file operations outside the project directory.
|
48
|
+
Configure in your Claude Desktop settings.
|
49
|
+
|
50
|
+
Benefits:
|
51
|
+
- Access global configuration files
|
52
|
+
- Move files between projects
|
53
|
+
- System-wide operations
|
54
|
+
SETUP
|
55
|
+
end
|
56
|
+
|
57
|
+
def generate_mcp_settings
|
58
|
+
settings = {
|
59
|
+
'mcpServers' => {}
|
60
|
+
}
|
61
|
+
|
62
|
+
if @config.desktop_commander_enabled
|
63
|
+
settings['mcpServers']['desktop-commander'] = {
|
64
|
+
'command' => 'npx',
|
65
|
+
'args' => ['-y', '@claude-ai/desktop-commander'],
|
66
|
+
'env' => {}
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
settings
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def sanitize_path(path)
|
76
|
+
# Only allow safe path characters
|
77
|
+
path.to_s.gsub(%r{[^a-zA-Z0-9_\-/.]}, '')
|
78
|
+
end
|
79
|
+
|
80
|
+
def sanitize_prompt(prompt)
|
81
|
+
# Remove potentially dangerous characters while preserving readability
|
82
|
+
prompt.to_s.gsub(/[`$\\]/, '').strip
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'colorize'
|
4
|
+
require_relative 'command_executor'
|
5
|
+
require_relative 'process_monitor'
|
6
|
+
|
7
|
+
module EnhanceSwarm
|
8
|
+
class Monitor
|
9
|
+
def initialize
|
10
|
+
@config = EnhanceSwarm.configuration
|
11
|
+
@process_monitor = ProcessMonitor.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def watch(interval: nil, timeout: nil)
|
15
|
+
interval ||= @config.monitor_interval
|
16
|
+
timeout ||= @config.monitor_timeout
|
17
|
+
|
18
|
+
# Delegate to built-in process monitor
|
19
|
+
@process_monitor.watch(interval: interval, timeout: timeout)
|
20
|
+
end
|
21
|
+
|
22
|
+
def status
|
23
|
+
# Delegate to built-in process monitor
|
24
|
+
@process_monitor.status
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,444 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module EnhanceSwarm
|
6
|
+
class NotificationManager
|
7
|
+
NOTIFICATION_TYPES = {
|
8
|
+
agent_completed: { priority: :high, desktop: true, sound: true },
|
9
|
+
agent_failed: { priority: :critical, desktop: true, sound: true },
|
10
|
+
agent_stuck: { priority: :high, desktop: true, sound: false },
|
11
|
+
coordination_complete: { priority: :medium, desktop: true, sound: true },
|
12
|
+
intervention_needed: { priority: :critical, desktop: true, sound: true },
|
13
|
+
progress_milestone: { priority: :low, desktop: false, sound: false }
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@enabled = true
|
18
|
+
@desktop_notifications = desktop_notifications_available?
|
19
|
+
@sound_enabled = sound_available?
|
20
|
+
@notification_history = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def notify(type, message, details = {})
|
24
|
+
return unless @enabled
|
25
|
+
|
26
|
+
notification = build_notification(type, message, details)
|
27
|
+
@notification_history << notification
|
28
|
+
|
29
|
+
# Console notification (always shown)
|
30
|
+
display_console_notification(notification)
|
31
|
+
|
32
|
+
# Desktop notification (if available and configured)
|
33
|
+
if should_show_desktop?(notification)
|
34
|
+
send_desktop_notification(notification)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Sound notification (if available and configured)
|
38
|
+
if should_play_sound?(notification)
|
39
|
+
play_notification_sound(notification[:priority])
|
40
|
+
end
|
41
|
+
|
42
|
+
# Log for automation tools
|
43
|
+
Logger.log_operation("notification_#{type}", 'sent', {
|
44
|
+
message: message,
|
45
|
+
priority: notification[:priority],
|
46
|
+
details: details
|
47
|
+
})
|
48
|
+
|
49
|
+
notification
|
50
|
+
end
|
51
|
+
|
52
|
+
def agent_completed(agent_id, role, duration, details = {})
|
53
|
+
message = "🎉 Agent '#{role}' completed successfully!"
|
54
|
+
|
55
|
+
notify(:agent_completed, message, {
|
56
|
+
agent_id: agent_id,
|
57
|
+
role: role,
|
58
|
+
duration: duration,
|
59
|
+
**details
|
60
|
+
})
|
61
|
+
|
62
|
+
if details[:output_path]
|
63
|
+
puts " View: enhance-swarm show #{agent_id}".colorize(:blue)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def agent_failed(agent_id, role, error, suggestions = [])
|
68
|
+
message = "❌ Agent '#{role}' failed: #{error}"
|
69
|
+
|
70
|
+
notify(:agent_failed, message, {
|
71
|
+
agent_id: agent_id,
|
72
|
+
role: role,
|
73
|
+
error: error,
|
74
|
+
suggestions: suggestions
|
75
|
+
})
|
76
|
+
|
77
|
+
if suggestions.any?
|
78
|
+
puts "\n💡 Quick fixes:".colorize(:yellow)
|
79
|
+
suggestions.each_with_index do |suggestion, index|
|
80
|
+
puts " #{index + 1}. #{suggestion}".colorize(:yellow)
|
81
|
+
end
|
82
|
+
puts "\nChoose [1-#{suggestions.length}] or [c]ustom command:".colorize(:yellow)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def agent_stuck(agent_id, role, last_activity, current_task = nil)
|
87
|
+
time_stuck = Time.now - last_activity
|
88
|
+
time_str = format_duration(time_stuck)
|
89
|
+
|
90
|
+
message = "⚠️ Agent '#{role}' stuck for #{time_str}"
|
91
|
+
|
92
|
+
notify(:agent_stuck, message, {
|
93
|
+
agent_id: agent_id,
|
94
|
+
role: role,
|
95
|
+
last_activity: last_activity,
|
96
|
+
time_stuck: time_stuck,
|
97
|
+
current_task: current_task
|
98
|
+
})
|
99
|
+
|
100
|
+
puts " Last activity: #{current_task || 'Unknown'}".colorize(:yellow)
|
101
|
+
puts " Action: enhance-swarm restart #{agent_id} [y/N]?".colorize(:blue)
|
102
|
+
end
|
103
|
+
|
104
|
+
def coordination_complete(summary)
|
105
|
+
total_agents = summary[:completed] + summary[:failed]
|
106
|
+
message = "✅ Coordination complete: #{summary[:completed]}/#{total_agents} agents succeeded"
|
107
|
+
|
108
|
+
notify(:coordination_complete, message, summary)
|
109
|
+
|
110
|
+
if summary[:failed] > 0
|
111
|
+
puts "\n⚠️ #{summary[:failed]} agent(s) failed. Review with: enhance-swarm review".colorize(:yellow)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def intervention_needed(reason, agent_id = nil, suggestions = [])
|
116
|
+
message = "🚨 Intervention needed: #{reason}"
|
117
|
+
|
118
|
+
notify(:intervention_needed, message, {
|
119
|
+
reason: reason,
|
120
|
+
agent_id: agent_id,
|
121
|
+
suggestions: suggestions
|
122
|
+
})
|
123
|
+
|
124
|
+
if suggestions.any?
|
125
|
+
puts "\nRecommended actions:".colorize(:red)
|
126
|
+
suggestions.each_with_index do |suggestion, index|
|
127
|
+
puts " #{index + 1}. #{suggestion}".colorize(:red)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def progress_milestone(milestone, progress_percentage, eta = nil)
|
133
|
+
message = "📍 #{milestone} (#{progress_percentage}% complete)"
|
134
|
+
|
135
|
+
notify(:progress_milestone, message, {
|
136
|
+
milestone: milestone,
|
137
|
+
progress: progress_percentage,
|
138
|
+
eta: eta
|
139
|
+
})
|
140
|
+
|
141
|
+
if eta
|
142
|
+
puts " ETA: #{eta.strftime('%H:%M:%S')}".colorize(:blue)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Background monitoring for stuck agents
|
147
|
+
def start_monitoring(agents)
|
148
|
+
return if @monitoring_thread&.alive?
|
149
|
+
|
150
|
+
@monitoring_thread = Thread.new do
|
151
|
+
monitor_agents(agents)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def stop_monitoring
|
156
|
+
@monitoring_thread&.kill
|
157
|
+
@monitoring_thread = nil
|
158
|
+
end
|
159
|
+
|
160
|
+
# Enable/disable notifications
|
161
|
+
def enable!
|
162
|
+
@enabled = true
|
163
|
+
puts "✅ Notifications enabled".colorize(:green)
|
164
|
+
end
|
165
|
+
|
166
|
+
def disable!
|
167
|
+
@enabled = false
|
168
|
+
puts "🔇 Notifications disabled".colorize(:yellow)
|
169
|
+
end
|
170
|
+
|
171
|
+
def enabled?
|
172
|
+
@enabled
|
173
|
+
end
|
174
|
+
|
175
|
+
# Notification history
|
176
|
+
def recent_notifications(limit = 10)
|
177
|
+
@notification_history.last(limit)
|
178
|
+
end
|
179
|
+
|
180
|
+
def clear_history
|
181
|
+
@notification_history.clear
|
182
|
+
end
|
183
|
+
|
184
|
+
# Test notification system
|
185
|
+
def test_notifications
|
186
|
+
puts "🔔 Testing notification capabilities...".colorize(:blue)
|
187
|
+
|
188
|
+
# Test console notification
|
189
|
+
puts "1. Console notifications: ✅ Available"
|
190
|
+
|
191
|
+
# Test desktop notifications
|
192
|
+
desktop_available = desktop_notifications_available?
|
193
|
+
puts "2. Desktop notifications: #{desktop_available ? '✅ Available' : '❌ Not available'}"
|
194
|
+
|
195
|
+
# Test sound notifications
|
196
|
+
sound_available = sound_available?
|
197
|
+
puts "3. Sound notifications: #{sound_available ? '✅ Available' : '❌ Not available'}"
|
198
|
+
|
199
|
+
puts "\n🧪 Running test notifications..."
|
200
|
+
sleep(1)
|
201
|
+
|
202
|
+
# Test different notification types
|
203
|
+
agent_completed('test-123', 'backend', 120, { success: true })
|
204
|
+
sleep(1)
|
205
|
+
|
206
|
+
progress_milestone('Test milestone reached', 75)
|
207
|
+
sleep(1)
|
208
|
+
|
209
|
+
puts "\n📊 Test Results:"
|
210
|
+
puts " Notifications sent: 2"
|
211
|
+
puts " History entries: #{@notification_history.count}"
|
212
|
+
puts " Status: #{@enabled ? 'Enabled' : 'Disabled'}"
|
213
|
+
|
214
|
+
if desktop_available || sound_available
|
215
|
+
puts "\nNote: Desktop/sound notifications may appear with a delay"
|
216
|
+
end
|
217
|
+
|
218
|
+
puts "\n✅ Notification test completed!"
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
def build_notification(type, message, details)
|
224
|
+
config = NOTIFICATION_TYPES[type] || { priority: :medium, desktop: false, sound: false }
|
225
|
+
|
226
|
+
{
|
227
|
+
type: type,
|
228
|
+
message: message,
|
229
|
+
details: details,
|
230
|
+
priority: config[:priority],
|
231
|
+
desktop: config[:desktop],
|
232
|
+
sound: config[:sound],
|
233
|
+
timestamp: Time.now
|
234
|
+
}
|
235
|
+
end
|
236
|
+
|
237
|
+
def display_console_notification(notification)
|
238
|
+
priority_colors = {
|
239
|
+
critical: :red,
|
240
|
+
high: :yellow,
|
241
|
+
medium: :blue,
|
242
|
+
low: :light_black
|
243
|
+
}
|
244
|
+
|
245
|
+
color = priority_colors[notification[:priority]] || :white
|
246
|
+
timestamp = notification[:timestamp].strftime('%H:%M:%S')
|
247
|
+
|
248
|
+
puts "[#{timestamp}] #{notification[:message]}".colorize(color)
|
249
|
+
end
|
250
|
+
|
251
|
+
def should_show_desktop?(notification)
|
252
|
+
@desktop_notifications && notification[:desktop] && notification[:priority] != :low
|
253
|
+
end
|
254
|
+
|
255
|
+
def should_play_sound?(notification)
|
256
|
+
@sound_enabled && notification[:sound] && [:critical, :high].include?(notification[:priority])
|
257
|
+
end
|
258
|
+
|
259
|
+
def send_desktop_notification(notification)
|
260
|
+
return unless @desktop_notifications
|
261
|
+
|
262
|
+
case RbConfig::CONFIG['host_os']
|
263
|
+
when /darwin/ # macOS
|
264
|
+
send_macos_notification(notification)
|
265
|
+
when /linux/ # Linux
|
266
|
+
send_linux_notification(notification)
|
267
|
+
when /mswin|mingw|cygwin/ # Windows
|
268
|
+
send_windows_notification(notification)
|
269
|
+
end
|
270
|
+
rescue StandardError => e
|
271
|
+
Logger.warn("Failed to send desktop notification: #{e.message}")
|
272
|
+
end
|
273
|
+
|
274
|
+
def send_macos_notification(notification)
|
275
|
+
title = "EnhanceSwarm"
|
276
|
+
subtitle = notification[:type].to_s.humanize
|
277
|
+
message = notification[:message]
|
278
|
+
|
279
|
+
# Use macOS osascript for notifications
|
280
|
+
script = <<~APPLESCRIPT
|
281
|
+
display notification "#{message}" with title "#{title}" subtitle "#{subtitle}"
|
282
|
+
APPLESCRIPT
|
283
|
+
|
284
|
+
CommandExecutor.execute('osascript', '-e', script)
|
285
|
+
end
|
286
|
+
|
287
|
+
def send_linux_notification(notification)
|
288
|
+
# Use notify-send if available
|
289
|
+
if CommandExecutor.command_available?('notify-send')
|
290
|
+
CommandExecutor.execute(
|
291
|
+
'notify-send',
|
292
|
+
'EnhanceSwarm',
|
293
|
+
notification[:message],
|
294
|
+
'--urgency=normal'
|
295
|
+
)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def send_windows_notification(notification)
|
300
|
+
# Use PowerShell toast notifications
|
301
|
+
if CommandExecutor.command_available?('powershell')
|
302
|
+
script = <<~POWERSHELL
|
303
|
+
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
|
304
|
+
$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
|
305
|
+
$template.SelectSingleNode('//text[@id="1"]').AppendChild($template.CreateTextNode('EnhanceSwarm'))
|
306
|
+
$template.SelectSingleNode('//text[@id="2"]').AppendChild($template.CreateTextNode('#{notification[:message]}'))
|
307
|
+
$toast = [Windows.UI.Notifications.ToastNotification]::new($template)
|
308
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('EnhanceSwarm').Show($toast)
|
309
|
+
POWERSHELL
|
310
|
+
|
311
|
+
CommandExecutor.execute('powershell', '-Command', script)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def play_notification_sound(priority)
|
316
|
+
return unless @sound_enabled
|
317
|
+
|
318
|
+
case RbConfig::CONFIG['host_os']
|
319
|
+
when /darwin/ # macOS
|
320
|
+
sound = priority == :critical ? 'Basso' : 'Ping'
|
321
|
+
CommandExecutor.execute('afplay', "/System/Library/Sounds/#{sound}.aiff")
|
322
|
+
when /linux/ # Linux
|
323
|
+
if CommandExecutor.command_available?('paplay')
|
324
|
+
# Use default system sound
|
325
|
+
CommandExecutor.execute('paplay', '/usr/share/sounds/alsa/Front_Left.wav')
|
326
|
+
elsif CommandExecutor.command_available?('aplay')
|
327
|
+
CommandExecutor.execute('aplay', '/usr/share/sounds/alsa/Front_Left.wav')
|
328
|
+
end
|
329
|
+
end
|
330
|
+
rescue StandardError => e
|
331
|
+
Logger.debug("Failed to play notification sound: #{e.message}")
|
332
|
+
end
|
333
|
+
|
334
|
+
def monitor_agents(agents)
|
335
|
+
while @enabled
|
336
|
+
agents.each do |agent|
|
337
|
+
check_agent_health(agent)
|
338
|
+
end
|
339
|
+
|
340
|
+
sleep(30) # Check every 30 seconds
|
341
|
+
end
|
342
|
+
rescue StandardError => e
|
343
|
+
Logger.error("Agent monitoring error: #{e.message}")
|
344
|
+
end
|
345
|
+
|
346
|
+
def check_agent_health(agent)
|
347
|
+
# Check if agent process is still running
|
348
|
+
unless process_running?(agent[:pid])
|
349
|
+
agent_failed(agent[:id], agent[:role], "Process terminated unexpectedly")
|
350
|
+
return
|
351
|
+
end
|
352
|
+
|
353
|
+
# Check for stuck agents (no activity for >10 minutes)
|
354
|
+
if agent[:last_activity] && (Time.now - agent[:last_activity]) > 600
|
355
|
+
agent_stuck(agent[:id], agent[:role], agent[:last_activity], agent[:current_task])
|
356
|
+
end
|
357
|
+
|
358
|
+
# Check for excessive memory usage
|
359
|
+
if agent[:memory_mb] && agent[:memory_mb] > 1000
|
360
|
+
intervention_needed(
|
361
|
+
"Agent '#{agent[:role]}' using excessive memory (#{agent[:memory_mb]}MB)",
|
362
|
+
agent[:id],
|
363
|
+
["enhance-swarm restart #{agent[:id]}", "enhance-swarm kill #{agent[:id]}"]
|
364
|
+
)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
def process_running?(pid)
|
369
|
+
Process.kill(0, pid)
|
370
|
+
true
|
371
|
+
rescue Errno::ESRCH
|
372
|
+
false
|
373
|
+
rescue Errno::EPERM
|
374
|
+
true # Process exists but we can't signal it
|
375
|
+
end
|
376
|
+
|
377
|
+
def desktop_notifications_available?
|
378
|
+
case RbConfig::CONFIG['host_os']
|
379
|
+
when /darwin/
|
380
|
+
CommandExecutor.command_available?('osascript')
|
381
|
+
when /linux/
|
382
|
+
CommandExecutor.command_available?('notify-send')
|
383
|
+
when /mswin|mingw|cygwin/
|
384
|
+
CommandExecutor.command_available?('powershell')
|
385
|
+
else
|
386
|
+
false
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def sound_available?
|
391
|
+
case RbConfig::CONFIG['host_os']
|
392
|
+
when /darwin/
|
393
|
+
CommandExecutor.command_available?('afplay')
|
394
|
+
when /linux/
|
395
|
+
CommandExecutor.command_available?('paplay') || CommandExecutor.command_available?('aplay')
|
396
|
+
else
|
397
|
+
false
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def format_duration(seconds)
|
402
|
+
if seconds < 60
|
403
|
+
"#{seconds.round}s"
|
404
|
+
elsif seconds < 3600
|
405
|
+
"#{(seconds / 60).round}m"
|
406
|
+
else
|
407
|
+
"#{(seconds / 3600).round(1)}h"
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
# Class methods for global access
|
412
|
+
def self.instance
|
413
|
+
@instance ||= new
|
414
|
+
end
|
415
|
+
|
416
|
+
def self.notify(*args)
|
417
|
+
instance.notify(*args)
|
418
|
+
end
|
419
|
+
|
420
|
+
def self.agent_completed(*args)
|
421
|
+
instance.agent_completed(*args)
|
422
|
+
end
|
423
|
+
|
424
|
+
def self.agent_failed(*args)
|
425
|
+
instance.agent_failed(*args)
|
426
|
+
end
|
427
|
+
|
428
|
+
def self.agent_stuck(*args)
|
429
|
+
instance.agent_stuck(*args)
|
430
|
+
end
|
431
|
+
|
432
|
+
def self.coordination_complete(*args)
|
433
|
+
instance.coordination_complete(*args)
|
434
|
+
end
|
435
|
+
|
436
|
+
def self.intervention_needed(*args)
|
437
|
+
instance.intervention_needed(*args)
|
438
|
+
end
|
439
|
+
|
440
|
+
def self.progress_milestone(*args)
|
441
|
+
instance.progress_milestone(*args)
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|