aidp 0.5.0 → 0.8.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +128 -151
  3. data/bin/aidp +1 -1
  4. data/lib/aidp/analysis/kb_inspector.rb +471 -0
  5. data/lib/aidp/analysis/seams.rb +159 -0
  6. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +480 -0
  7. data/lib/aidp/analysis/tree_sitter_scan.rb +686 -0
  8. data/lib/aidp/analyze/error_handler.rb +2 -78
  9. data/lib/aidp/analyze/json_file_storage.rb +292 -0
  10. data/lib/aidp/analyze/progress.rb +12 -0
  11. data/lib/aidp/analyze/progress_visualizer.rb +12 -17
  12. data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
  13. data/lib/aidp/analyze/runner.rb +256 -87
  14. data/lib/aidp/analyze/steps.rb +6 -0
  15. data/lib/aidp/cli/jobs_command.rb +103 -435
  16. data/lib/aidp/cli.rb +317 -191
  17. data/lib/aidp/config.rb +298 -10
  18. data/lib/aidp/debug_logger.rb +195 -0
  19. data/lib/aidp/debug_mixin.rb +187 -0
  20. data/lib/aidp/execute/progress.rb +9 -0
  21. data/lib/aidp/execute/runner.rb +221 -40
  22. data/lib/aidp/execute/steps.rb +17 -7
  23. data/lib/aidp/execute/workflow_selector.rb +211 -0
  24. data/lib/aidp/harness/completion_checker.rb +268 -0
  25. data/lib/aidp/harness/condition_detector.rb +1526 -0
  26. data/lib/aidp/harness/config_loader.rb +373 -0
  27. data/lib/aidp/harness/config_manager.rb +382 -0
  28. data/lib/aidp/harness/config_schema.rb +1006 -0
  29. data/lib/aidp/harness/config_validator.rb +355 -0
  30. data/lib/aidp/harness/configuration.rb +477 -0
  31. data/lib/aidp/harness/enhanced_runner.rb +494 -0
  32. data/lib/aidp/harness/error_handler.rb +616 -0
  33. data/lib/aidp/harness/provider_config.rb +423 -0
  34. data/lib/aidp/harness/provider_factory.rb +306 -0
  35. data/lib/aidp/harness/provider_manager.rb +1269 -0
  36. data/lib/aidp/harness/provider_type_checker.rb +88 -0
  37. data/lib/aidp/harness/runner.rb +411 -0
  38. data/lib/aidp/harness/state/errors.rb +28 -0
  39. data/lib/aidp/harness/state/metrics.rb +219 -0
  40. data/lib/aidp/harness/state/persistence.rb +128 -0
  41. data/lib/aidp/harness/state/provider_state.rb +132 -0
  42. data/lib/aidp/harness/state/ui_state.rb +68 -0
  43. data/lib/aidp/harness/state/workflow_state.rb +123 -0
  44. data/lib/aidp/harness/state_manager.rb +586 -0
  45. data/lib/aidp/harness/status_display.rb +888 -0
  46. data/lib/aidp/harness/ui/base.rb +16 -0
  47. data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
  48. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
  49. data/lib/aidp/harness/ui/error_handler.rb +132 -0
  50. data/lib/aidp/harness/ui/frame_manager.rb +361 -0
  51. data/lib/aidp/harness/ui/job_monitor.rb +500 -0
  52. data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
  53. data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
  54. data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
  55. data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
  56. data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
  57. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
  58. data/lib/aidp/harness/ui/progress_display.rb +280 -0
  59. data/lib/aidp/harness/ui/question_collector.rb +141 -0
  60. data/lib/aidp/harness/ui/spinner_group.rb +184 -0
  61. data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
  62. data/lib/aidp/harness/ui/status_manager.rb +312 -0
  63. data/lib/aidp/harness/ui/status_widget.rb +280 -0
  64. data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
  65. data/lib/aidp/harness/user_interface.rb +2381 -0
  66. data/lib/aidp/provider_manager.rb +131 -7
  67. data/lib/aidp/providers/anthropic.rb +28 -109
  68. data/lib/aidp/providers/base.rb +170 -0
  69. data/lib/aidp/providers/cursor.rb +52 -183
  70. data/lib/aidp/providers/gemini.rb +24 -109
  71. data/lib/aidp/providers/macos_ui.rb +99 -5
  72. data/lib/aidp/providers/opencode.rb +194 -0
  73. data/lib/aidp/storage/csv_storage.rb +172 -0
  74. data/lib/aidp/storage/file_manager.rb +214 -0
  75. data/lib/aidp/storage/json_storage.rb +140 -0
  76. data/lib/aidp/version.rb +1 -1
  77. data/lib/aidp.rb +56 -35
  78. data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
  79. data/templates/COMMON/AGENT_BASE.md +11 -0
  80. data/templates/EXECUTE/00_PRD.md +4 -4
  81. data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
  82. data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
  83. data/templates/EXECUTE/08_TASKS.md +4 -4
  84. data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
  85. data/templates/README.md +279 -0
  86. data/templates/aidp-development.yml.example +373 -0
  87. data/templates/aidp-minimal.yml.example +48 -0
  88. data/templates/aidp-production.yml.example +475 -0
  89. data/templates/aidp.yml.example +598 -0
  90. metadata +106 -64
  91. data/lib/aidp/analyze/agent_personas.rb +0 -71
  92. data/lib/aidp/analyze/agent_tool_executor.rb +0 -445
  93. data/lib/aidp/analyze/data_retention_manager.rb +0 -426
  94. data/lib/aidp/analyze/database.rb +0 -260
  95. data/lib/aidp/analyze/dependencies.rb +0 -335
  96. data/lib/aidp/analyze/export_manager.rb +0 -425
  97. data/lib/aidp/analyze/focus_guidance.rb +0 -517
  98. data/lib/aidp/analyze/incremental_analyzer.rb +0 -543
  99. data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
  100. data/lib/aidp/analyze/large_analysis_progress.rb +0 -504
  101. data/lib/aidp/analyze/memory_manager.rb +0 -365
  102. data/lib/aidp/analyze/metrics_storage.rb +0 -336
  103. data/lib/aidp/analyze/parallel_processor.rb +0 -460
  104. data/lib/aidp/analyze/performance_optimizer.rb +0 -694
  105. data/lib/aidp/analyze/repository_chunker.rb +0 -704
  106. data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
  107. data/lib/aidp/analyze/storage.rb +0 -662
  108. data/lib/aidp/analyze/tool_configuration.rb +0 -456
  109. data/lib/aidp/analyze/tool_modernization.rb +0 -750
  110. data/lib/aidp/database/pg_adapter.rb +0 -148
  111. data/lib/aidp/database_config.rb +0 -69
  112. data/lib/aidp/database_connection.rb +0 -72
  113. data/lib/aidp/database_migration.rb +0 -158
  114. data/lib/aidp/job_manager.rb +0 -41
  115. data/lib/aidp/jobs/base_job.rb +0 -47
  116. data/lib/aidp/jobs/provider_execution_job.rb +0 -96
  117. data/lib/aidp/project_detector.rb +0 -117
  118. data/lib/aidp/providers/agent_supervisor.rb +0 -348
  119. data/lib/aidp/providers/supervised_base.rb +0 -317
  120. data/lib/aidp/providers/supervised_cursor.rb +0 -22
  121. data/lib/aidp/sync.rb +0 -13
  122. data/lib/aidp/workspace.rb +0 -19
@@ -0,0 +1,500 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require "pastel"
5
+ require_relative "base"
6
+ require_relative "status_manager"
7
+ require_relative "frame_manager"
8
+
9
+ module Aidp
10
+ module Harness
11
+ module UI
12
+ # Real-time job monitoring and status tracking
13
+ class JobMonitor < Base
14
+ class JobMonitorError < StandardError; end
15
+ class JobNotFoundError < JobMonitorError; end
16
+ class MonitorError < JobMonitorError; end
17
+
18
+ JOB_STATUSES = {
19
+ pending: "Pending",
20
+ running: "Running",
21
+ completed: "Completed",
22
+ failed: "Failed",
23
+ cancelled: "Cancelled",
24
+ retrying: "Retrying"
25
+ }.freeze
26
+
27
+ JOB_PRIORITIES = {
28
+ low: "Low",
29
+ normal: "Normal",
30
+ high: "High",
31
+ urgent: "Urgent"
32
+ }.freeze
33
+
34
+ def initialize(ui_components = {})
35
+ super()
36
+ @prompt = TTY::Prompt.new
37
+ @pastel = Pastel.new
38
+ @status_manager = ui_components[:status_manager] || StatusManager.new
39
+ @frame_manager = ui_components[:frame_manager] || FrameManager.new
40
+ @formatter = ui_components[:formatter] || JobMonitorFormatter.new
41
+
42
+ @jobs = {}
43
+ @job_history = []
44
+ @monitoring_active = false
45
+ @monitor_thread = nil
46
+ @monitor_mutex = Mutex.new
47
+ @update_callbacks = []
48
+ end
49
+
50
+ def register_job(job_id, job_data)
51
+ validate_job_id(job_id)
52
+ validate_job_data(job_data)
53
+
54
+ @monitor_mutex.synchronize do
55
+ job = create_job_entry(job_id, job_data)
56
+ @jobs[job_id] = job
57
+ record_job_event(job_id, :registered, job_data)
58
+ notify_callbacks(:job_registered, job)
59
+ end
60
+ rescue => e
61
+ raise MonitorError, "Failed to register job: #{e.message}"
62
+ end
63
+
64
+ def update_job_status(job_id, status, additional_data = {})
65
+ validate_job_id(job_id)
66
+ validate_job_status(status)
67
+
68
+ @monitor_mutex.synchronize do
69
+ job = @jobs[job_id]
70
+ raise JobNotFoundError, "Job not found: #{job_id}" unless job
71
+
72
+ old_status = job[:status]
73
+ job[:status] = status
74
+ job[:last_updated] = Time.now
75
+ job.merge!(additional_data)
76
+
77
+ record_job_event(job_id, :status_changed, {from: old_status, to: status})
78
+ notify_callbacks(:job_status_changed, job, old_status)
79
+ end
80
+ rescue JobNotFoundError => e
81
+ raise e
82
+ rescue => e
83
+ raise MonitorError, "Failed to update job status: #{e.message}"
84
+ end
85
+
86
+ def get_job_status(job_id)
87
+ validate_job_id(job_id)
88
+
89
+ @monitor_mutex.synchronize do
90
+ job = @jobs[job_id]
91
+ raise JobNotFoundError, "Job not found: #{job_id}" unless job
92
+ job.dup
93
+ end
94
+ end
95
+
96
+ def has_job?(job_id)
97
+ validate_job_id(job_id)
98
+ @monitor_mutex.synchronize { @jobs.key?(job_id) }
99
+ end
100
+
101
+ def get_all_jobs
102
+ @monitor_mutex.synchronize { @jobs.dup }
103
+ end
104
+
105
+ def get_jobs_by_status(status)
106
+ validate_job_status(status)
107
+
108
+ @monitor_mutex.synchronize do
109
+ @jobs.select { |_, job| job[:status] == status }
110
+ end
111
+ rescue JobMonitorError => e
112
+ raise MonitorError, "Failed to get jobs by status: #{e.message}"
113
+ end
114
+
115
+ def get_jobs_by_priority(priority)
116
+ validate_job_priority(priority)
117
+
118
+ @monitor_mutex.synchronize do
119
+ @jobs.select { |_, job| job[:priority] == priority }
120
+ end
121
+ end
122
+
123
+ def start_monitoring(interval_seconds = 1.0)
124
+ return if @monitoring_active
125
+
126
+ @monitoring_active = true
127
+ @monitor_thread = Thread.new do
128
+ monitoring_loop(interval_seconds)
129
+ end
130
+
131
+ @prompt.say(@formatter.format_monitoring_started(interval_seconds))
132
+ rescue => e
133
+ raise MonitorError, "Failed to start monitoring: #{e.message}"
134
+ end
135
+
136
+ def stop_monitoring
137
+ return unless @monitoring_active
138
+
139
+ @monitoring_active = false
140
+ @monitor_thread&.join
141
+ @monitor_thread = nil
142
+
143
+ @prompt.say(@formatter.format_monitoring_stopped)
144
+ rescue => e
145
+ raise MonitorError, "Failed to stop monitoring: #{e.message}"
146
+ end
147
+
148
+ def monitoring_active?
149
+ @monitoring_active
150
+ end
151
+
152
+ def add_update_callback(callback)
153
+ validate_callback(callback)
154
+ @update_callbacks << callback
155
+ rescue JobMonitorError => e
156
+ raise MonitorError, "Failed to add update callback: #{e.message}"
157
+ end
158
+
159
+ def remove_update_callback(callback)
160
+ @update_callbacks.delete(callback)
161
+ end
162
+
163
+ def get_monitoring_summary
164
+ @monitor_mutex.synchronize do
165
+ {
166
+ total_jobs: @jobs.size,
167
+ jobs_by_status: @jobs.values.map { |job| job[:status] }.tally,
168
+ jobs_by_priority: @jobs.values.map { |job| job[:priority] }.tally,
169
+ monitoring_active: @monitoring_active,
170
+ total_events: @job_history.size,
171
+ last_update: @job_history.last&.dig(:timestamp)
172
+ }
173
+ end
174
+ end
175
+
176
+ def display_job_status(job_id)
177
+ job = get_job_status(job_id)
178
+ @frame_manager.section("Job Status: #{job_id}") do
179
+ display_job_details(job)
180
+ end
181
+ end
182
+
183
+ def display_all_jobs
184
+ @frame_manager.section("All Jobs") do
185
+ display_jobs_table(@jobs)
186
+ end
187
+ end
188
+
189
+ def display_jobs_by_status(status)
190
+ jobs = get_jobs_by_status(status)
191
+ @frame_manager.section("Jobs with Status: #{status}") do
192
+ display_jobs_table(jobs)
193
+ end
194
+ end
195
+
196
+ private
197
+
198
+ def validate_job_id(job_id)
199
+ raise JobMonitorError, "Job ID cannot be empty" if job_id.to_s.strip.empty?
200
+ end
201
+
202
+ def validate_job_data(job_data)
203
+ raise JobMonitorError, "Job data must be a hash" unless job_data.is_a?(Hash)
204
+ end
205
+
206
+ def validate_job_status(status)
207
+ unless JOB_STATUSES.key?(status)
208
+ raise JobMonitorError, "Invalid job status: #{status}. Must be one of: #{JOB_STATUSES.keys.join(", ")}"
209
+ end
210
+ end
211
+
212
+ def validate_job_priority(priority)
213
+ unless JOB_PRIORITIES.key?(priority)
214
+ raise JobMonitorError, "Invalid job priority: #{priority}. Must be one of: #{JOB_PRIORITIES.keys.join(", ")}"
215
+ end
216
+ end
217
+
218
+ def validate_callback(callback)
219
+ unless callback.respond_to?(:call)
220
+ raise JobMonitorError, "Callback must respond to :call"
221
+ end
222
+ end
223
+
224
+ def create_job_entry(job_id, job_data)
225
+ {
226
+ id: job_id,
227
+ status: job_data[:status] || :pending,
228
+ priority: job_data[:priority] || :normal,
229
+ created_at: Time.now,
230
+ last_updated: Time.now,
231
+ progress: job_data[:progress] || 0,
232
+ total_steps: job_data[:total_steps] || 1,
233
+ current_step: job_data[:current_step] || 0,
234
+ error_message: job_data[:error_message],
235
+ retry_count: job_data[:retry_count] || 0,
236
+ max_retries: job_data[:max_retries] || 3,
237
+ estimated_completion: job_data[:estimated_completion],
238
+ metadata: job_data[:metadata] || {}
239
+ }
240
+ end
241
+
242
+ def record_job_event(job_id, event_type, event_data)
243
+ event = {
244
+ job_id: job_id,
245
+ event_type: event_type,
246
+ timestamp: Time.now,
247
+ data: event_data
248
+ }
249
+
250
+ @job_history << event
251
+ end
252
+
253
+ def notify_callbacks(event_type, job, additional_data = nil)
254
+ @update_callbacks.each do |callback|
255
+ callback.call(event_type, job, additional_data)
256
+ rescue => e
257
+ @prompt.say(@formatter.format_callback_error(callback, e.message))
258
+ end
259
+ end
260
+
261
+ def monitoring_loop(interval_seconds)
262
+ loop do
263
+ break unless @monitoring_active
264
+
265
+ begin
266
+ perform_monitoring_cycle
267
+ sleep(interval_seconds)
268
+ rescue => e
269
+ @prompt.say(@formatter.format_monitoring_error(e.message))
270
+ end
271
+ end
272
+ end
273
+
274
+ def perform_monitoring_cycle
275
+ # Perform monitoring tasks like checking for stuck jobs, updating progress, etc.
276
+ check_for_stuck_jobs
277
+ update_job_progress
278
+ cleanup_completed_jobs
279
+ end
280
+
281
+ def check_for_stuck_jobs
282
+ stuck_threshold = 300 # 5 minutes
283
+ current_time = Time.now
284
+
285
+ @jobs.each do |job_id, job|
286
+ if job[:status] == :running && (current_time - job[:last_updated]) > stuck_threshold
287
+ update_job_status(job_id, :failed, {error_message: "Job appears to be stuck"})
288
+ end
289
+ end
290
+ end
291
+
292
+ def update_job_progress
293
+ # Update progress for running jobs
294
+ @jobs.each do |job_id, job|
295
+ if job[:status] == :running && job[:current_step] < job[:total_steps]
296
+ # Simulate progress update - in real implementation, this would come from the job
297
+ new_progress = [(job[:current_step] + 1).to_f / job[:total_steps] * 100, 100].min
298
+ update_job_status(job_id, :running, {progress: new_progress})
299
+ end
300
+ end
301
+ end
302
+
303
+ def cleanup_completed_jobs
304
+ # Clean up old completed jobs
305
+ cleanup_threshold = 3600 # 1 hour
306
+ current_time = Time.now
307
+
308
+ jobs_to_remove = @jobs.select do |job_id, job|
309
+ (job[:status] == :completed || job[:status] == :failed) &&
310
+ (current_time - job[:last_updated]) > cleanup_threshold
311
+ end
312
+
313
+ jobs_to_remove.each do |job_id, _|
314
+ @jobs.delete(job_id)
315
+ record_job_event(job_id, :cleaned_up, {reason: "Old completed job"})
316
+ end
317
+ end
318
+
319
+ def display_job_details(job)
320
+ details = []
321
+ details << "Job ID: #{job[:id]}"
322
+ details << "Status: #{@formatter.format_job_status(job[:status])}"
323
+ details << "Priority: #{@formatter.format_job_priority(job[:priority])}"
324
+ details << "Progress: #{@formatter.format_job_progress(job[:progress])}"
325
+ details << "Created: #{job[:created_at]}"
326
+ details << "Last Updated: #{job[:last_updated]}"
327
+
328
+ if job[:error_message]
329
+ details << "Error: #{@formatter.format_job_error(job[:error_message])}"
330
+ end
331
+
332
+ if job[:retry_count] > 0
333
+ details << "Retries: #{job[:retry_count]}/#{job[:max_retries]}"
334
+ end
335
+
336
+ details.join("\n")
337
+ end
338
+
339
+ def display_jobs_table(jobs)
340
+ if jobs.empty?
341
+ return "No jobs found."
342
+ end
343
+
344
+ lines = []
345
+ lines << "ID".ljust(20) + "Status".ljust(12) + "Priority".ljust(10) + "Progress".ljust(10) + "Created"
346
+ lines << "-" * 70
347
+
348
+ jobs.each do |job_id, job|
349
+ status = @formatter.format_job_status_short(job[:status])
350
+ priority = @formatter.format_job_priority_short(job[:priority])
351
+ progress = @formatter.format_job_progress_short(job[:progress])
352
+ created = job[:created_at].strftime("%H:%M:%S")
353
+
354
+ lines << job_id.to_s.ljust(20) + status.to_s.ljust(12) + priority.to_s.ljust(10) + progress.to_s.ljust(10) + created.to_s
355
+ end
356
+
357
+ lines.join("\n")
358
+ end
359
+ end
360
+
361
+ # Formats job monitor display
362
+ class JobMonitorFormatter
363
+ def initialize
364
+ @pastel = Pastel.new
365
+ end
366
+
367
+ def format_job_status(status)
368
+ case status
369
+ when :pending
370
+ @pastel.yellow("⏳ Pending")
371
+ when :running
372
+ @pastel.blue("🔄 Running")
373
+ when :completed
374
+ @pastel.green("✅ Completed")
375
+ when :failed
376
+ @pastel.red("❌ Failed")
377
+ when :cancelled
378
+ @pastel.red("🚫 Cancelled")
379
+ when :retrying
380
+ @pastel.yellow("🔄 Retrying")
381
+ else
382
+ @pastel.blue("❓ #{status.to_s.capitalize}")
383
+ end
384
+ end
385
+
386
+ def format_job_status_short(status)
387
+ case status
388
+ when :pending
389
+ @pastel.yellow("⏳")
390
+ when :running
391
+ @pastel.blue("🔄")
392
+ when :completed
393
+ @pastel.green("✅")
394
+ when :failed
395
+ @pastel.red("❌")
396
+ when :cancelled
397
+ @pastel.red("🚫")
398
+ when :retrying
399
+ @pastel.yellow("🔄")
400
+ else
401
+ @pastel.blue("❓")
402
+ end
403
+ end
404
+
405
+ def format_job_priority(priority)
406
+ case priority
407
+ when :low
408
+ @pastel.blue("🔽 Low")
409
+ when :normal
410
+ @pastel.blue("➡️ Normal")
411
+ when :high
412
+ @pastel.yellow("🔼 High")
413
+ when :urgent
414
+ @pastel.red("🚨 Urgent")
415
+ else
416
+ @pastel.blue("❓ #{priority.to_s.capitalize}")
417
+ end
418
+ end
419
+
420
+ def format_job_priority_short(priority)
421
+ case priority
422
+ when :low
423
+ @pastel.blue("🔽")
424
+ when :normal
425
+ @pastel.blue("➡️")
426
+ when :high
427
+ @pastel.yellow("🔼")
428
+ when :urgent
429
+ @pastel.red("🚨")
430
+ else
431
+ @pastel.blue("❓")
432
+ end
433
+ end
434
+
435
+ def format_job_progress(progress)
436
+ progress_int = progress.to_i
437
+ if progress_int >= 100
438
+ @pastel.green("100%")
439
+ elsif progress_int >= 75
440
+ @pastel.blue("#{progress_int}%")
441
+ elsif progress_int >= 50
442
+ @pastel.yellow("#{progress_int}%")
443
+ else
444
+ @pastel.blue("#{progress_int}%")
445
+ end
446
+ end
447
+
448
+ def format_job_progress_short(progress)
449
+ progress_int = progress.to_i
450
+ if progress_int >= 100
451
+ @pastel.green("100%")
452
+ elsif progress_int >= 75
453
+ @pastel.blue("#{progress_int}%")
454
+ elsif progress_int >= 50
455
+ @pastel.yellow("#{progress_int}%")
456
+ else
457
+ @pastel.blue("#{progress_int}%")
458
+ end
459
+ end
460
+
461
+ def format_job_error(error_message)
462
+ @pastel.red("❌ #{error_message}")
463
+ end
464
+
465
+ def format_monitoring_started(interval_seconds)
466
+ @pastel.green("✅ Job monitoring started (interval: #{interval_seconds}s)")
467
+ end
468
+
469
+ def format_monitoring_stopped
470
+ @pastel.red("❌ Job monitoring stopped")
471
+ end
472
+
473
+ def format_monitoring_error(error_message)
474
+ @pastel.red("❌ Monitoring error: #{error_message}")
475
+ end
476
+
477
+ def format_callback_error(callback, error_message)
478
+ @pastel.red("❌ Callback error: #{error_message}")
479
+ end
480
+
481
+ def format_monitoring_summary(summary)
482
+ result = []
483
+ result << @pastel.bold(@pastel.blue("📊 Job Monitoring Summary"))
484
+ result << "Total jobs: #{@pastel.bold(summary[:total_jobs])}"
485
+ result << "Monitoring: #{summary[:monitoring_active] ? "Active" : "Inactive"}"
486
+ result << "Total events: #{@pastel.blue(summary[:total_events])}"
487
+
488
+ if summary[:jobs_by_status].any?
489
+ result << "Jobs by status:"
490
+ summary[:jobs_by_status].each do |status, count|
491
+ result << " #{@pastel.blue("#{status}: #{count}")}"
492
+ end
493
+ end
494
+
495
+ result.join("\n")
496
+ end
497
+ end
498
+ end
499
+ end
500
+ end