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
@@ -1,487 +1,155 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tty-cursor"
4
- require "tty-screen"
5
- require "tty-table"
3
+ require "tty-box"
4
+ require "pastel"
6
5
  require "io/console"
7
- require "que"
8
6
  require "json"
9
7
  require_relative "terminal_io"
8
+ require_relative "../storage/file_manager"
10
9
 
11
10
  module Aidp
12
11
  class CLI
13
12
  class JobsCommand
14
13
  def initialize(input: $stdin, output: $stdout)
15
14
  @io = TerminalIO.new(input, output)
16
- @cursor = TTY::Cursor
17
- @screen_width = TTY::Screen.width
18
- @screen_height = TTY::Screen.height
15
+ @pastel = Pastel.new
19
16
  @running = true
20
17
  @view_mode = :list
21
18
  @selected_job_id = nil
22
19
  @jobs_displayed = false # Track if we've displayed jobs in interactive mode
20
+ @file_manager = Aidp::Storage::FileManager.new(File.join(Dir.pwd, ".aidp"))
21
+ @screen_width = 80 # Default screen width
23
22
  end
24
23
 
25
24
  def run
26
- # Initialize Que connection
27
- setup_database_connection
28
-
29
- # Start the UI loop with timeout
30
- Timeout.timeout(60) do
31
- while @running
32
- case @view_mode
33
- when :list
34
- result = render_job_list
35
- if result == :exit
36
- # Exit immediately when no jobs are found
37
- break
38
- end
39
- handle_input
40
- sleep_for_refresh unless @running
41
- when :details
42
- render_job_details
43
- handle_input
44
- sleep_for_refresh unless @running
45
- when :output
46
- render_job_output
47
- handle_input
48
- sleep_for_refresh unless @running
49
- end
50
- end
51
- end
52
- rescue Timeout::Error
53
- @io.puts "Command timed out"
54
- @running = false
55
- ensure
56
- # Only clear screen and show cursor if we were in interactive mode
57
- # (i.e., if we had jobs to display and were in a real terminal)
58
- if @view_mode == :list && @jobs_displayed
59
- @io.print @cursor.clear_screen
60
- @io.print @cursor.show
61
- end
62
- end
63
-
64
- private
65
-
66
- def setup_database_connection
67
- # Skip database setup in test mode if we're mocking
68
- return if ENV["RACK_ENV"] == "test" && ENV["MOCK_DATABASE"] == "true"
69
-
70
- dbname = (ENV["RACK_ENV"] == "test") ? "aidp_test" : (ENV["AIDP_DB_NAME"] || "aidp")
71
-
72
- # Use Sequel for connection pooling with timeout
73
- Timeout.timeout(10) do
74
- Que.connection = Sequel.connect(
75
- adapter: "postgres",
76
- host: ENV["AIDP_DB_HOST"] || "localhost",
77
- port: ENV["AIDP_DB_PORT"] || 5432,
78
- database: dbname,
79
- user: ENV["AIDP_DB_USER"] || ENV["USER"],
80
- password: ENV["AIDP_DB_PASSWORD"],
81
- max_connections: 10,
82
- pool_timeout: 30
83
- )
84
-
85
- Que.migrate!(version: Que::Migrations::CURRENT_VERSION)
86
- end
87
- rescue Timeout::Error
88
- @io.puts "Database connection timed out"
89
- raise
90
- rescue => e
91
- @io.puts "Error connecting to database: #{e.message}"
92
- raise
93
- end
94
-
95
- def render_job_list
96
- jobs = fetch_jobs
25
+ # Simple harness jobs display
26
+ jobs = fetch_harness_jobs
97
27
 
98
28
  if jobs.empty?
99
- # Don't clear screen when no jobs - just show the message
100
- @io.puts "Background Jobs"
29
+ @io.puts "Harness Jobs"
101
30
  @io.puts "-" * @screen_width
102
31
  @io.puts
103
- @io.puts "No jobs are currently running"
104
- return :exit
105
- else
106
- # Clear screen and hide cursor only when we have jobs to display
107
- @io.print(@cursor.hide)
108
- @io.print(@cursor.clear_screen)
109
- @io.print(@cursor.move_to(0, 0))
110
- @jobs_displayed = true # Mark that we've displayed jobs
111
-
112
- # Print header
113
- @io.puts "Background Jobs"
114
- @io.puts "-" * @screen_width
32
+ @io.puts "No harness jobs found"
115
33
  @io.puts
116
-
117
- # Create table
118
- table = TTY::Table.new(
119
- header: ["ID", "Job", "Queue", "Status", "Runtime", "Error"],
120
- rows: jobs.map do |job|
121
- [
122
- job[:id].to_s,
123
- job[:job_class]&.split("::")&.last || "Unknown",
124
- job[:queue] || "default",
125
- job_status(job),
126
- format_runtime(job),
127
- truncate_error(job[:last_error_message])
128
- ]
129
- end
130
- )
131
-
132
- # Render table
133
- @io.puts table.render(:unicode, padding: [0, 1], width: @screen_width)
134
- end
135
-
136
- @io.puts
137
- @io.puts "Commands: (d)etails, (o)utput, (r)etry, (k)ill, (q)uit"
138
- :continue
139
- end
140
-
141
- def render_job_details
142
- return switch_to_list unless @selected_job_id
143
-
144
- job = fetch_job(@selected_job_id)
145
- return switch_to_list unless job
146
-
147
- # Clear screen and hide cursor
148
- @io.print(@cursor.hide)
149
- @io.print(@cursor.clear_screen)
150
- @io.print(@cursor.move_to(0, 0))
151
-
152
- # Print header
153
- @io.puts "Job Details - ID: #{@selected_job_id}"
154
- @io.puts "-" * @screen_width
155
- @io.puts
156
-
157
- # Print job details
158
- @io.puts "Class: #{job[:job_class]}"
159
- @io.puts "Queue: #{job[:queue]}"
160
- @io.puts "Status: #{job_status(job)}"
161
- @io.puts "Runtime: #{format_runtime(job)}"
162
- @io.puts "Started: #{job[:run_at]}"
163
- @io.puts "Finished: #{job[:finished_at]}"
164
- @io.puts "Attempts: #{job[:error_count]}"
165
- @io.puts
166
- @io.puts "Error:" if job[:last_error_message]
167
- @io.puts job[:last_error_message] if job[:last_error_message]
168
-
169
- @io.puts
170
- @io.puts "Commands: (b)ack, (o)utput, (r)etry, (k)ill, (q)uit"
171
- end
172
-
173
- def render_job_output
174
- return switch_to_list unless @selected_job_id
175
-
176
- job = fetch_job(@selected_job_id)
177
- return switch_to_list unless job
178
-
179
- # Clear screen and hide cursor
180
- @io.print(@cursor.hide)
181
- @io.print(@cursor.clear_screen)
182
- @io.print(@cursor.move_to(0, 0))
183
-
184
- # Print header
185
- @io.puts "Job Output - ID: #{@selected_job_id}"
186
- @io.puts "-" * @screen_width
187
- @io.puts
188
-
189
- # Get job output
190
- output = get_job_output(@selected_job_id)
191
-
192
- if output.empty?
193
- @io.puts "No output available for this job."
194
- @io.puts
195
- @io.puts "This could mean:"
196
- @io.puts "- The job hasn't started yet"
197
- @io.puts "- The job is still running but hasn't produced output"
198
- @io.puts "- The job completed without output"
34
+ @io.puts "Harness jobs are background tasks that run during harness mode."
35
+ @io.puts "They are stored as JSON files in the .aidp/harness_logs/ directory."
199
36
  else
200
- @io.puts "Recent Output:"
201
- @io.puts "-" * 20
202
- @io.puts output
37
+ render_harness_jobs(jobs)
203
38
  end
204
-
205
- @io.puts
206
- @io.puts "Commands: (b)ack, (r)efresh, (q)uit"
207
39
  end
208
40
 
209
- def handle_input
210
- if @io.ready?
211
- char = @io.getch
212
- return if char.nil? || char.empty?
213
-
214
- case char.downcase
215
- when "q"
216
- @running = false
217
- when "d"
218
- handle_details_command
219
- when "o"
220
- handle_output_command
221
- when "r"
222
- handle_retry_command
223
- when "k"
224
- handle_kill_command
225
- when "b"
226
- switch_to_list
227
- end
228
- end
229
- end
230
-
231
- def handle_details_command
232
- return unless @view_mode == :list
233
-
234
- @io.print "Enter job ID: "
235
- job_id = @io.gets.chomp
236
- if job_exists?(job_id)
237
- @selected_job_id = job_id
238
- @view_mode = :details
239
- end
240
- end
241
-
242
- def handle_output_command
243
- job_id = (@view_mode == :details) ? @selected_job_id : nil
244
-
245
- unless job_id
246
- @io.print "Enter job ID: "
247
- job_id = @io.gets.chomp
248
- end
249
-
250
- if job_exists?(job_id)
251
- @selected_job_id = job_id
252
- @view_mode = :output
253
- end
254
- end
255
-
256
- def handle_retry_command
257
- job_id = (@view_mode == :details) ? @selected_job_id : nil
258
-
259
- unless job_id
260
- @io.print "Enter job ID: "
261
- job_id = @io.gets.chomp
262
- end
41
+ private
263
42
 
264
- if job_exists?(job_id)
265
- job = fetch_job(job_id)
266
- if job[:error_count].to_i > 0
267
- Que.execute(
268
- <<~SQL,
269
- UPDATE que_jobs
270
- SET error_count = 0,
271
- last_error_message = NULL,
272
- finished_at = NULL,
273
- expired_at = NULL
274
- WHERE id = $1
275
- SQL
276
- [job_id]
277
- )
278
- @io.puts "Job #{job_id} has been queued for retry"
43
+ # Fetch harness jobs from file-based storage
44
+ def fetch_harness_jobs
45
+ jobs = []
46
+
47
+ # Get all harness log files
48
+ harness_logs_dir = File.join(Dir.pwd, ".aidp", "harness_logs")
49
+ return jobs unless Dir.exist?(harness_logs_dir)
50
+
51
+ Dir.glob(File.join(harness_logs_dir, "*.json")).each do |log_file|
52
+ log_data = JSON.parse(File.read(log_file))
53
+ job_id = File.basename(log_file, ".json")
54
+
55
+ # Get job metadata from the log
56
+ job_info = {
57
+ id: job_id,
58
+ status: determine_job_status(log_data),
59
+ created_at: log_data["created_at"],
60
+ message: log_data["message"],
61
+ level: log_data["level"],
62
+ metadata: log_data["metadata"] || {}
63
+ }
64
+
65
+ jobs << job_info
66
+ rescue JSON::ParserError => e
67
+ @io.puts "Warning: Could not parse harness log #{log_file}: #{e.message}" if ENV["AIDP_DEBUG"]
68
+ end
69
+
70
+ # Sort by creation time (newest first)
71
+ jobs.sort_by { |job| job[:created_at] }.reverse
72
+ end
73
+
74
+ # Determine job status from log data
75
+ def determine_job_status(log_data)
76
+ case log_data["level"]
77
+ when "error"
78
+ "failed"
79
+ when "info"
80
+ if log_data["message"].include?("completed")
81
+ "completed"
82
+ elsif log_data["message"].include?("retrying")
83
+ "retrying"
279
84
  else
280
- @io.puts "Job #{job_id} has no errors to retry"
85
+ "running"
281
86
  end
282
- sleep 2
87
+ else
88
+ "unknown"
283
89
  end
284
90
  end
285
91
 
286
- def handle_kill_command
287
- job_id = (@view_mode == :details) ? @selected_job_id : nil
288
-
289
- unless job_id
290
- @io.print "Enter job ID: "
291
- job_id = @io.gets.chomp
292
- end
293
-
294
- if job_exists?(job_id)
295
- job = fetch_job(job_id)
296
-
297
- # Only allow killing running jobs
298
- if job_status(job) == "running"
299
- @io.print "Are you sure you want to kill job #{job_id}? (y/N): "
300
- confirmation = @io.gets.chomp.downcase
92
+ # Render harness jobs in a simple table
93
+ def render_harness_jobs(jobs)
94
+ @io.puts "Harness Jobs"
95
+ @io.puts "-" * @screen_width
96
+ @io.puts
301
97
 
302
- if confirmation == "y" || confirmation == "yes"
303
- kill_job(job_id)
304
- @io.puts "Job #{job_id} has been killed"
305
- sleep 2
306
- else
307
- @io.puts "Job kill cancelled"
308
- sleep 1
309
- end
310
- else
311
- @io.puts "Only running jobs can be killed. Job #{job_id} is #{job_status(job)}"
312
- sleep 2
98
+ # Create job content for TTY::Box
99
+ job_content = []
100
+ jobs.each do |job|
101
+ status_icon = case job[:status]
102
+ when "completed" then "✅"
103
+ when "running" then "🔄"
104
+ when "failed" then "❌"
105
+ when "pending" then "⏳"
106
+ else "❓"
313
107
  end
314
- end
315
- end
316
-
317
- def switch_to_list
318
- @view_mode = :list
319
- @selected_job_id = nil
320
- end
321
-
322
- def fetch_jobs
323
- # For testing, return empty array if no database connection
324
- return [] if ENV["RACK_ENV"] == "test" && !Que.connection
325
- return [] if ENV["RACK_ENV"] == "test" && ENV["MOCK_DATABASE"] == "true"
326
-
327
- Timeout.timeout(10) do
328
- Que.execute(
329
- <<~SQL
330
- SELECT *
331
- FROM que_jobs
332
- ORDER BY
333
- CASE
334
- WHEN finished_at IS NULL AND error_count = 0 THEN 1 -- Running
335
- WHEN error_count > 0 THEN 2 -- Failed
336
- ELSE 3 -- Completed
337
- END,
338
- id DESC
339
- SQL
340
- )
341
- end
342
- rescue Timeout::Error
343
- @io.puts "Database query timed out"
344
- []
345
- rescue Sequel::DatabaseError => e
346
- @io.puts "Error fetching jobs: #{e.message}"
347
- []
348
- end
349
-
350
- def fetch_job(job_id)
351
- Timeout.timeout(5) do
352
- Que.execute("SELECT * FROM que_jobs WHERE id = $1", [job_id]).first
353
- end
354
- rescue Timeout::Error
355
- @io.puts "Database query timed out"
356
- nil
357
- rescue Sequel::DatabaseError => e
358
- @io.puts "Error fetching job #{job_id}: #{e.message}"
359
- nil
360
- end
361
-
362
- def job_exists?(job_id)
363
- fetch_job(job_id) != nil
364
- end
365
-
366
- def sleep_for_refresh
367
- sleep 1
368
- end
369
108
 
370
- def job_status(job)
371
- return "unknown" unless job
372
-
373
- if job[:finished_at]
374
- (job[:error_count].to_i > 0) ? "failed" : "completed"
375
- else
376
- "running"
377
- end
378
- end
379
-
380
- def format_runtime(job)
381
- return "unknown" unless job
382
-
383
- if job[:finished_at] && job[:run_at]
384
- finished_at = job[:finished_at].is_a?(Time) ? job[:finished_at] : Time.parse(job[:finished_at])
385
- run_at = job[:run_at].is_a?(Time) ? job[:run_at] : Time.parse(job[:run_at])
386
- duration = finished_at - run_at
387
- minutes = (duration / 60).to_i
388
- seconds = (duration % 60).to_i
389
- (minutes > 0) ? "#{minutes}m #{seconds}s" : "#{seconds}s"
390
- elsif job[:run_at]
391
- run_at = job[:run_at].is_a?(Time) ? job[:run_at] : Time.parse(job[:run_at])
392
- duration = Time.now - run_at
393
- minutes = (duration / 60).to_i
394
- seconds = (duration % 60).to_i
395
- (minutes > 0) ? "#{minutes}m #{seconds}s" : "#{seconds}s"
396
- else
397
- "pending"
109
+ job_info = []
110
+ job_info << "#{status_icon} #{job[:id][0..7]}"
111
+ job_info << "Status: #{@pastel.bold(job[:status])}"
112
+ job_info << "Created: #{format_time(job[:created_at])}"
113
+ job_info << "Message: #{truncate_message(job[:message])}"
114
+ job_content << job_info.join("\n")
398
115
  end
399
- rescue => e
400
- "error (#{e.message})"
401
- end
402
116
 
403
- def truncate_error(error)
404
- return nil unless error
117
+ # Create main box with all jobs
118
+ box = TTY::Box.frame(
119
+ job_content.join("\n\n"),
120
+ title: {top_left: "Background Jobs"},
121
+ border: :thick,
122
+ padding: [1, 2]
123
+ )
124
+ puts box
405
125
 
406
- max_length = @screen_width - 60 # Account for other columns
407
- if error.length > max_length
408
- "#{error[0..max_length - 4]}..."
409
- else
410
- error
411
- end
126
+ @io.puts
127
+ @io.puts "Total: #{jobs.length} harness job(s)"
128
+ @io.puts
129
+ @io.puts "Note: Harness jobs are stored as JSON files in .aidp/harness_logs/"
412
130
  end
413
131
 
414
- def get_job_output(job_id)
415
- # Try to get output from various sources
416
- output = []
132
+ # Format timestamp for display
133
+ def format_time(timestamp)
134
+ return "unknown" unless timestamp
417
135
 
418
- # 1. Check if there's a result stored in analysis_results table
419
136
  begin
420
- result = Que.execute(
421
- "SELECT data FROM analysis_results WHERE step_name = $1",
422
- ["job_#{job_id}"]
423
- ).first
424
-
425
- if result && result["data"]
426
- data = JSON.parse(result["data"])
427
- output << "Result: #{data["output"]}" if data["output"]
428
- end
137
+ time = Time.parse(timestamp)
138
+ time.strftime("%Y-%m-%d %H:%M:%S")
429
139
  rescue
430
- # Ignore errors - table might not exist
140
+ timestamp
431
141
  end
432
-
433
- # 2. Check for any recent log entries
434
- begin
435
- logs = Que.execute(
436
- "SELECT message FROM que_jobs WHERE id = $1 AND last_error_message IS NOT NULL",
437
- [job_id]
438
- ).first
439
-
440
- if logs && logs["last_error_message"]
441
- output << "Error: #{logs["last_error_message"]}"
442
- end
443
- rescue
444
- # Ignore errors
445
- end
446
-
447
- # 3. Check if job appears to be hung
448
- job = fetch_job(job_id)
449
- if job && job_status(job) == "running"
450
- run_at = job[:run_at].is_a?(Time) ? job[:run_at] : Time.parse(job[:run_at])
451
- duration = Time.now - run_at
452
-
453
- if duration > 300 # 5 minutes
454
- output << "⚠️ WARNING: Job has been running for #{format_duration(duration)}"
455
- output << " This job may be hung or stuck."
456
- end
457
- end
458
-
459
- output.join("\n")
460
- end
461
-
462
- def kill_job(job_id)
463
- # Mark the job as finished with an error to stop it
464
- Que.execute(
465
- <<~SQL,
466
- UPDATE que_jobs
467
- SET finished_at = NOW(),
468
- last_error_message = 'Job killed by user',
469
- error_count = error_count + 1
470
- WHERE id = $1
471
- SQL
472
- [job_id]
473
- )
474
142
  end
475
143
 
476
- def format_duration(seconds)
477
- minutes = (seconds / 60).to_i
478
- hours = (minutes / 60).to_i
479
- minutes %= 60
144
+ # Truncate message for table display
145
+ def truncate_message(message)
146
+ return "N/A" unless message
480
147
 
481
- if hours > 0
482
- "#{hours}h #{minutes}m"
148
+ max_length = @screen_width - 50 # Account for other columns
149
+ if message.length > max_length
150
+ "#{message[0..max_length - 4]}..."
483
151
  else
484
- "#{minutes}m"
152
+ message
485
153
  end
486
154
  end
487
155
  end