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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.enhance_swarm/agent_scripts/frontend_agent.md +39 -0
  3. data/.enhance_swarm/user_patterns.json +37 -0
  4. data/CHANGELOG.md +184 -0
  5. data/LICENSE +21 -0
  6. data/PRODUCTION_TEST_LOG.md +502 -0
  7. data/README.md +905 -0
  8. data/Rakefile +28 -0
  9. data/USAGE_EXAMPLES.md +477 -0
  10. data/examples/enhance_workflow.md +346 -0
  11. data/examples/rails_project.md +253 -0
  12. data/exe/enhance-swarm +30 -0
  13. data/lib/enhance_swarm/additional_commands.rb +299 -0
  14. data/lib/enhance_swarm/agent_communicator.rb +460 -0
  15. data/lib/enhance_swarm/agent_reviewer.rb +283 -0
  16. data/lib/enhance_swarm/agent_spawner.rb +462 -0
  17. data/lib/enhance_swarm/cleanup_manager.rb +245 -0
  18. data/lib/enhance_swarm/cli.rb +1592 -0
  19. data/lib/enhance_swarm/command_executor.rb +78 -0
  20. data/lib/enhance_swarm/configuration.rb +324 -0
  21. data/lib/enhance_swarm/control_agent.rb +307 -0
  22. data/lib/enhance_swarm/dependency_validator.rb +195 -0
  23. data/lib/enhance_swarm/error_recovery.rb +785 -0
  24. data/lib/enhance_swarm/generator.rb +194 -0
  25. data/lib/enhance_swarm/interrupt_handler.rb +512 -0
  26. data/lib/enhance_swarm/logger.rb +106 -0
  27. data/lib/enhance_swarm/mcp_integration.rb +85 -0
  28. data/lib/enhance_swarm/monitor.rb +28 -0
  29. data/lib/enhance_swarm/notification_manager.rb +444 -0
  30. data/lib/enhance_swarm/orchestrator.rb +313 -0
  31. data/lib/enhance_swarm/output_streamer.rb +281 -0
  32. data/lib/enhance_swarm/process_monitor.rb +266 -0
  33. data/lib/enhance_swarm/progress_tracker.rb +215 -0
  34. data/lib/enhance_swarm/project_analyzer.rb +612 -0
  35. data/lib/enhance_swarm/resource_manager.rb +177 -0
  36. data/lib/enhance_swarm/retry_handler.rb +40 -0
  37. data/lib/enhance_swarm/session_manager.rb +247 -0
  38. data/lib/enhance_swarm/signal_handler.rb +95 -0
  39. data/lib/enhance_swarm/smart_defaults.rb +708 -0
  40. data/lib/enhance_swarm/task_integration.rb +150 -0
  41. data/lib/enhance_swarm/task_manager.rb +174 -0
  42. data/lib/enhance_swarm/version.rb +5 -0
  43. data/lib/enhance_swarm/visual_dashboard.rb +555 -0
  44. data/lib/enhance_swarm/web_ui.rb +211 -0
  45. data/lib/enhance_swarm.rb +69 -0
  46. data/setup.sh +86 -0
  47. data/sig/enhance_swarm.rbs +4 -0
  48. data/templates/claude/CLAUDE.md +160 -0
  49. data/templates/claude/MCP.md +117 -0
  50. data/templates/claude/PERSONAS.md +114 -0
  51. data/templates/claude/RULES.md +221 -0
  52. data/test_builtin_functionality.rb +121 -0
  53. data/test_core_components.rb +156 -0
  54. data/test_real_claude_integration.rb +285 -0
  55. data/test_security.rb +150 -0
  56. data/test_smart_defaults.rb +155 -0
  57. data/test_task_integration.rb +173 -0
  58. data/test_web_ui.rb +245 -0
  59. data/web/assets/css/main.css +645 -0
  60. data/web/assets/js/kanban.js +499 -0
  61. data/web/assets/js/main.js +525 -0
  62. data/web/templates/dashboard.html.erb +226 -0
  63. data/web/templates/kanban.html.erb +193 -0
  64. 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