aidp 0.7.0 → 0.8.1

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -214
  3. data/bin/aidp +1 -1
  4. data/lib/aidp/analysis/kb_inspector.rb +38 -23
  5. data/lib/aidp/analysis/seams.rb +2 -31
  6. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +1 -13
  7. data/lib/aidp/analysis/tree_sitter_scan.rb +3 -20
  8. data/lib/aidp/analyze/error_handler.rb +2 -75
  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/cli/jobs_command.rb +100 -432
  15. data/lib/aidp/cli.rb +309 -239
  16. data/lib/aidp/config.rb +298 -10
  17. data/lib/aidp/debug_logger.rb +195 -0
  18. data/lib/aidp/debug_mixin.rb +187 -0
  19. data/lib/aidp/execute/progress.rb +9 -0
  20. data/lib/aidp/execute/runner.rb +221 -40
  21. data/lib/aidp/execute/steps.rb +17 -7
  22. data/lib/aidp/execute/workflow_selector.rb +211 -0
  23. data/lib/aidp/harness/completion_checker.rb +268 -0
  24. data/lib/aidp/harness/condition_detector.rb +1526 -0
  25. data/lib/aidp/harness/config_loader.rb +373 -0
  26. data/lib/aidp/harness/config_manager.rb +382 -0
  27. data/lib/aidp/harness/config_schema.rb +1006 -0
  28. data/lib/aidp/harness/config_validator.rb +355 -0
  29. data/lib/aidp/harness/configuration.rb +477 -0
  30. data/lib/aidp/harness/enhanced_runner.rb +494 -0
  31. data/lib/aidp/harness/error_handler.rb +616 -0
  32. data/lib/aidp/harness/provider_config.rb +423 -0
  33. data/lib/aidp/harness/provider_factory.rb +306 -0
  34. data/lib/aidp/harness/provider_manager.rb +1269 -0
  35. data/lib/aidp/harness/provider_type_checker.rb +88 -0
  36. data/lib/aidp/harness/runner.rb +411 -0
  37. data/lib/aidp/harness/state/errors.rb +28 -0
  38. data/lib/aidp/harness/state/metrics.rb +219 -0
  39. data/lib/aidp/harness/state/persistence.rb +128 -0
  40. data/lib/aidp/harness/state/provider_state.rb +132 -0
  41. data/lib/aidp/harness/state/ui_state.rb +68 -0
  42. data/lib/aidp/harness/state/workflow_state.rb +123 -0
  43. data/lib/aidp/harness/state_manager.rb +586 -0
  44. data/lib/aidp/harness/status_display.rb +888 -0
  45. data/lib/aidp/harness/ui/base.rb +16 -0
  46. data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
  47. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
  48. data/lib/aidp/harness/ui/error_handler.rb +132 -0
  49. data/lib/aidp/harness/ui/frame_manager.rb +361 -0
  50. data/lib/aidp/harness/ui/job_monitor.rb +500 -0
  51. data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
  52. data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
  53. data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
  54. data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
  55. data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
  56. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
  57. data/lib/aidp/harness/ui/progress_display.rb +280 -0
  58. data/lib/aidp/harness/ui/question_collector.rb +141 -0
  59. data/lib/aidp/harness/ui/spinner_group.rb +184 -0
  60. data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
  61. data/lib/aidp/harness/ui/status_manager.rb +312 -0
  62. data/lib/aidp/harness/ui/status_widget.rb +280 -0
  63. data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
  64. data/lib/aidp/harness/user_interface.rb +2381 -0
  65. data/lib/aidp/provider_manager.rb +131 -7
  66. data/lib/aidp/providers/anthropic.rb +28 -103
  67. data/lib/aidp/providers/base.rb +170 -0
  68. data/lib/aidp/providers/cursor.rb +52 -181
  69. data/lib/aidp/providers/gemini.rb +24 -107
  70. data/lib/aidp/providers/macos_ui.rb +99 -5
  71. data/lib/aidp/providers/opencode.rb +194 -0
  72. data/lib/aidp/storage/csv_storage.rb +172 -0
  73. data/lib/aidp/storage/file_manager.rb +214 -0
  74. data/lib/aidp/storage/json_storage.rb +140 -0
  75. data/lib/aidp/version.rb +1 -1
  76. data/lib/aidp.rb +54 -39
  77. data/templates/COMMON/AGENT_BASE.md +11 -0
  78. data/templates/EXECUTE/00_PRD.md +4 -4
  79. data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
  80. data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
  81. data/templates/EXECUTE/08_TASKS.md +4 -4
  82. data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
  83. data/templates/README.md +279 -0
  84. data/templates/aidp-development.yml.example +373 -0
  85. data/templates/aidp-minimal.yml.example +48 -0
  86. data/templates/aidp-production.yml.example +475 -0
  87. data/templates/aidp.yml.example +598 -0
  88. metadata +93 -69
  89. data/lib/aidp/analyze/agent_personas.rb +0 -71
  90. data/lib/aidp/analyze/agent_tool_executor.rb +0 -439
  91. data/lib/aidp/analyze/data_retention_manager.rb +0 -421
  92. data/lib/aidp/analyze/database.rb +0 -260
  93. data/lib/aidp/analyze/dependencies.rb +0 -335
  94. data/lib/aidp/analyze/export_manager.rb +0 -418
  95. data/lib/aidp/analyze/focus_guidance.rb +0 -517
  96. data/lib/aidp/analyze/incremental_analyzer.rb +0 -533
  97. data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
  98. data/lib/aidp/analyze/large_analysis_progress.rb +0 -499
  99. data/lib/aidp/analyze/memory_manager.rb +0 -339
  100. data/lib/aidp/analyze/metrics_storage.rb +0 -336
  101. data/lib/aidp/analyze/parallel_processor.rb +0 -454
  102. data/lib/aidp/analyze/performance_optimizer.rb +0 -691
  103. data/lib/aidp/analyze/repository_chunker.rb +0 -697
  104. data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
  105. data/lib/aidp/analyze/storage.rb +0 -655
  106. data/lib/aidp/analyze/tool_configuration.rb +0 -441
  107. data/lib/aidp/analyze/tool_modernization.rb +0 -750
  108. data/lib/aidp/database/pg_adapter.rb +0 -148
  109. data/lib/aidp/database_config.rb +0 -69
  110. data/lib/aidp/database_connection.rb +0 -72
  111. data/lib/aidp/job_manager.rb +0 -41
  112. data/lib/aidp/jobs/base_job.rb +0 -45
  113. data/lib/aidp/jobs/provider_execution_job.rb +0 -83
  114. data/lib/aidp/project_detector.rb +0 -117
  115. data/lib/aidp/providers/agent_supervisor.rb +0 -348
  116. data/lib/aidp/providers/supervised_base.rb +0 -317
  117. data/lib/aidp/providers/supervised_cursor.rb +0 -22
  118. data/lib/aidp/sync.rb +0 -13
  119. 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
- end
91
-
92
- def render_job_list
93
- jobs = fetch_jobs
25
+ # Simple harness jobs display
26
+ jobs = fetch_harness_jobs
94
27
 
95
28
  if jobs.empty?
96
- # Don't clear screen when no jobs - just show the message
97
- @io.puts "Background Jobs"
29
+ @io.puts "Harness Jobs"
98
30
  @io.puts "-" * @screen_width
99
31
  @io.puts
100
- @io.puts "No jobs are currently running"
101
- return :exit
102
- else
103
- # Clear screen and hide cursor only when we have jobs to display
104
- @io.print(@cursor.hide)
105
- @io.print(@cursor.clear_screen)
106
- @io.print(@cursor.move_to(0, 0))
107
- @jobs_displayed = true # Mark that we've displayed jobs
108
-
109
- # Print header
110
- @io.puts "Background Jobs"
111
- @io.puts "-" * @screen_width
32
+ @io.puts "No harness jobs found"
112
33
  @io.puts
113
-
114
- # Create table
115
- table = TTY::Table.new(
116
- header: ["ID", "Job", "Queue", "Status", "Runtime", "Error"],
117
- rows: jobs.map do |job|
118
- [
119
- job[:id].to_s,
120
- job[:job_class]&.split("::")&.last || "Unknown",
121
- job[:queue] || "default",
122
- job_status(job),
123
- format_runtime(job),
124
- truncate_error(job[:last_error_message])
125
- ]
126
- end
127
- )
128
-
129
- # Render table
130
- @io.puts table.render(:unicode, padding: [0, 1], width: @screen_width)
131
- end
132
-
133
- @io.puts
134
- @io.puts "Commands: (d)etails, (o)utput, (r)etry, (k)ill, (q)uit"
135
- :continue
136
- end
137
-
138
- def render_job_details
139
- return switch_to_list unless @selected_job_id
140
-
141
- job = fetch_job(@selected_job_id)
142
- return switch_to_list unless job
143
-
144
- # Clear screen and hide cursor
145
- @io.print(@cursor.hide)
146
- @io.print(@cursor.clear_screen)
147
- @io.print(@cursor.move_to(0, 0))
148
-
149
- # Print header
150
- @io.puts "Job Details - ID: #{@selected_job_id}"
151
- @io.puts "-" * @screen_width
152
- @io.puts
153
-
154
- # Print job details
155
- @io.puts "Class: #{job[:job_class]}"
156
- @io.puts "Queue: #{job[:queue]}"
157
- @io.puts "Status: #{job_status(job)}"
158
- @io.puts "Runtime: #{format_runtime(job)}"
159
- @io.puts "Started: #{job[:run_at]}"
160
- @io.puts "Finished: #{job[:finished_at]}"
161
- @io.puts "Attempts: #{job[:error_count]}"
162
- @io.puts
163
- @io.puts "Error:" if job[:last_error_message]
164
- @io.puts job[:last_error_message] if job[:last_error_message]
165
-
166
- @io.puts
167
- @io.puts "Commands: (b)ack, (o)utput, (r)etry, (k)ill, (q)uit"
168
- end
169
-
170
- def render_job_output
171
- return switch_to_list unless @selected_job_id
172
-
173
- job = fetch_job(@selected_job_id)
174
- return switch_to_list unless job
175
-
176
- # Clear screen and hide cursor
177
- @io.print(@cursor.hide)
178
- @io.print(@cursor.clear_screen)
179
- @io.print(@cursor.move_to(0, 0))
180
-
181
- # Print header
182
- @io.puts "Job Output - ID: #{@selected_job_id}"
183
- @io.puts "-" * @screen_width
184
- @io.puts
185
-
186
- # Get job output
187
- output = get_job_output(@selected_job_id)
188
-
189
- if output.empty?
190
- @io.puts "No output available for this job."
191
- @io.puts
192
- @io.puts "This could mean:"
193
- @io.puts "- The job hasn't started yet"
194
- @io.puts "- The job is still running but hasn't produced output"
195
- @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."
196
36
  else
197
- @io.puts "Recent Output:"
198
- @io.puts "-" * 20
199
- @io.puts output
200
- end
201
-
202
- @io.puts
203
- @io.puts "Commands: (b)ack, (r)efresh, (q)uit"
204
- end
205
-
206
- def handle_input
207
- if @io.ready?
208
- char = @io.getch
209
- return if char.nil? || char.empty?
210
-
211
- case char.downcase
212
- when "q"
213
- @running = false
214
- when "d"
215
- handle_details_command
216
- when "o"
217
- handle_output_command
218
- when "r"
219
- handle_retry_command
220
- when "k"
221
- handle_kill_command
222
- when "b"
223
- switch_to_list
224
- end
37
+ render_harness_jobs(jobs)
225
38
  end
226
39
  end
227
40
 
228
- def handle_details_command
229
- return unless @view_mode == :list
230
-
231
- @io.print "Enter job ID: "
232
- job_id = @io.gets.chomp
233
- if job_exists?(job_id)
234
- @selected_job_id = job_id
235
- @view_mode = :details
236
- end
237
- end
238
-
239
- def handle_output_command
240
- job_id = (@view_mode == :details) ? @selected_job_id : nil
41
+ private
241
42
 
242
- unless job_id
243
- @io.print "Enter job ID: "
244
- job_id = @io.gets.chomp
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"]
245
68
  end
246
69
 
247
- if job_exists?(job_id)
248
- @selected_job_id = job_id
249
- @view_mode = :output
250
- end
70
+ # Sort by creation time (newest first)
71
+ jobs.sort_by { |job| job[:created_at] }.reverse
251
72
  end
252
73
 
253
- def handle_retry_command
254
- job_id = (@view_mode == :details) ? @selected_job_id : nil
255
-
256
- unless job_id
257
- @io.print "Enter job ID: "
258
- job_id = @io.gets.chomp
259
- end
260
-
261
- if job_exists?(job_id)
262
- job = fetch_job(job_id)
263
- if job[:error_count].to_i > 0
264
- Que.execute(
265
- <<~SQL,
266
- UPDATE que_jobs
267
- SET error_count = 0,
268
- last_error_message = NULL,
269
- finished_at = NULL,
270
- expired_at = NULL
271
- WHERE id = $1
272
- SQL
273
- [job_id]
274
- )
275
- @io.puts "Job #{job_id} has been queued for retry"
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"
276
84
  else
277
- @io.puts "Job #{job_id} has no errors to retry"
85
+ "running"
278
86
  end
279
- sleep 2
87
+ else
88
+ "unknown"
280
89
  end
281
90
  end
282
91
 
283
- def handle_kill_command
284
- job_id = (@view_mode == :details) ? @selected_job_id : nil
285
-
286
- unless job_id
287
- @io.print "Enter job ID: "
288
- job_id = @io.gets.chomp
289
- end
290
-
291
- if job_exists?(job_id)
292
- job = fetch_job(job_id)
293
-
294
- # Only allow killing running jobs
295
- if job_status(job) == "running"
296
- @io.print "Are you sure you want to kill job #{job_id}? (y/N): "
297
- 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
298
97
 
299
- if confirmation == "y" || confirmation == "yes"
300
- kill_job(job_id)
301
- @io.puts "Job #{job_id} has been killed"
302
- sleep 2
303
- else
304
- @io.puts "Job kill cancelled"
305
- sleep 1
306
- end
307
- else
308
- @io.puts "Only running jobs can be killed. Job #{job_id} is #{job_status(job)}"
309
- 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 "❓"
310
107
  end
311
- end
312
- end
313
108
 
314
- def switch_to_list
315
- @view_mode = :list
316
- @selected_job_id = nil
317
- end
318
-
319
- def fetch_jobs
320
- # For testing, return empty array if no database connection
321
- return [] if ENV["RACK_ENV"] == "test" && !Que.connection
322
- return [] if ENV["RACK_ENV"] == "test" && ENV["MOCK_DATABASE"] == "true"
323
-
324
- Timeout.timeout(10) do
325
- Que.execute(
326
- <<~SQL
327
- SELECT *
328
- FROM que_jobs
329
- ORDER BY
330
- CASE
331
- WHEN finished_at IS NULL AND error_count = 0 THEN 1 -- Running
332
- WHEN error_count > 0 THEN 2 -- Failed
333
- ELSE 3 -- Completed
334
- END,
335
- id DESC
336
- SQL
337
- )
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")
338
115
  end
339
- rescue Timeout::Error
340
- @io.puts "Database query timed out"
341
- []
342
- rescue Sequel::DatabaseError => e
343
- @io.puts "Error fetching jobs: #{e.message}"
344
- []
345
- end
346
-
347
- def fetch_job(job_id)
348
- Timeout.timeout(5) do
349
- Que.execute("SELECT * FROM que_jobs WHERE id = $1", [job_id]).first
350
- end
351
- rescue Timeout::Error
352
- @io.puts "Database query timed out"
353
- nil
354
- rescue Sequel::DatabaseError => e
355
- @io.puts "Error fetching job #{job_id}: #{e.message}"
356
- nil
357
- end
358
116
 
359
- def job_exists?(job_id)
360
- fetch_job(job_id) != nil
361
- end
362
-
363
- def sleep_for_refresh
364
- sleep 1
365
- end
366
-
367
- def job_status(job)
368
- return "unknown" unless job
369
-
370
- if job[:finished_at]
371
- (job[:error_count].to_i > 0) ? "failed" : "completed"
372
- else
373
- "running"
374
- end
375
- end
376
-
377
- def format_runtime(job)
378
- return "unknown" unless job
379
-
380
- if job[:finished_at] && job[:run_at]
381
- finished_at = job[:finished_at].is_a?(Time) ? job[:finished_at] : Time.parse(job[:finished_at])
382
- run_at = job[:run_at].is_a?(Time) ? job[:run_at] : Time.parse(job[:run_at])
383
- duration = finished_at - run_at
384
- minutes = (duration / 60).to_i
385
- seconds = (duration % 60).to_i
386
- (minutes > 0) ? "#{minutes}m #{seconds}s" : "#{seconds}s"
387
- elsif job[:run_at]
388
- run_at = job[:run_at].is_a?(Time) ? job[:run_at] : Time.parse(job[:run_at])
389
- duration = Time.now - run_at
390
- minutes = (duration / 60).to_i
391
- seconds = (duration % 60).to_i
392
- (minutes > 0) ? "#{minutes}m #{seconds}s" : "#{seconds}s"
393
- else
394
- "pending"
395
- end
396
- end
397
-
398
- def truncate_error(error)
399
- 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
400
125
 
401
- max_length = @screen_width - 60 # Account for other columns
402
- if error.length > max_length
403
- "#{error[0..max_length - 4]}..."
404
- else
405
- error
406
- 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/"
407
130
  end
408
131
 
409
- def get_job_output(job_id)
410
- # Try to get output from various sources
411
- output = []
412
-
413
- # 1. Check if there's a result stored in analysis_results table
414
- begin
415
- result = Que.execute(
416
- "SELECT data FROM analysis_results WHERE step_name = $1",
417
- ["job_#{job_id}"]
418
- ).first
419
-
420
- if result && result["data"]
421
- data = JSON.parse(result["data"])
422
- output << "Result: #{data["output"]}" if data["output"]
423
- end
424
- rescue Sequel::DatabaseError, PG::Error => e
425
- # Database error - table might not exist
426
- @io.puts "Warning: Could not fetch job result: #{e.message}" if ENV["AIDP_DEBUG"]
427
- rescue JSON::ParserError => e
428
- # JSON parse error
429
- @io.puts "Warning: Could not parse job result data: #{e.message}" if ENV["AIDP_DEBUG"]
430
- end
132
+ # Format timestamp for display
133
+ def format_time(timestamp)
134
+ return "unknown" unless timestamp
431
135
 
432
- # 2. Check for any recent log entries
433
136
  begin
434
- logs = Que.execute(
435
- "SELECT message FROM que_jobs WHERE id = $1 AND last_error_message IS NOT NULL",
436
- [job_id]
437
- ).first
438
-
439
- if logs && logs["last_error_message"]
440
- output << "Error: #{logs["last_error_message"]}"
441
- end
442
- rescue Sequel::DatabaseError, PG::Error => e
443
- # Database error fetching logs - continue with diagnostic
444
- @io.puts "Warning: Could not fetch job logs: #{e.message}" if ENV["AIDP_DEBUG"]
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
137
+ time = Time.parse(timestamp)
138
+ time.strftime("%Y-%m-%d %H:%M:%S")
139
+ rescue
140
+ timestamp
457
141
  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