aidp 0.3.0 → 0.7.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +191 -5
  3. data/lib/aidp/analysis/kb_inspector.rb +456 -0
  4. data/lib/aidp/analysis/seams.rb +188 -0
  5. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +493 -0
  6. data/lib/aidp/analysis/tree_sitter_scan.rb +703 -0
  7. data/lib/aidp/analyze/agent_personas.rb +1 -1
  8. data/lib/aidp/analyze/agent_tool_executor.rb +5 -11
  9. data/lib/aidp/analyze/data_retention_manager.rb +0 -5
  10. data/lib/aidp/analyze/database.rb +99 -82
  11. data/lib/aidp/analyze/error_handler.rb +12 -79
  12. data/lib/aidp/analyze/export_manager.rb +0 -7
  13. data/lib/aidp/analyze/focus_guidance.rb +2 -2
  14. data/lib/aidp/analyze/incremental_analyzer.rb +1 -11
  15. data/lib/aidp/analyze/large_analysis_progress.rb +0 -5
  16. data/lib/aidp/analyze/memory_manager.rb +34 -60
  17. data/lib/aidp/analyze/metrics_storage.rb +336 -0
  18. data/lib/aidp/analyze/parallel_processor.rb +0 -6
  19. data/lib/aidp/analyze/performance_optimizer.rb +0 -3
  20. data/lib/aidp/analyze/prioritizer.rb +2 -2
  21. data/lib/aidp/analyze/repository_chunker.rb +14 -21
  22. data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
  23. data/lib/aidp/analyze/runner.rb +107 -191
  24. data/lib/aidp/analyze/steps.rb +35 -30
  25. data/lib/aidp/analyze/storage.rb +233 -178
  26. data/lib/aidp/analyze/tool_configuration.rb +21 -36
  27. data/lib/aidp/cli/jobs_command.rb +489 -0
  28. data/lib/aidp/cli/terminal_io.rb +52 -0
  29. data/lib/aidp/cli.rb +160 -45
  30. data/lib/aidp/core_ext/class_attribute.rb +36 -0
  31. data/lib/aidp/database/pg_adapter.rb +148 -0
  32. data/lib/aidp/database_config.rb +69 -0
  33. data/lib/aidp/database_connection.rb +72 -0
  34. data/lib/aidp/execute/runner.rb +65 -92
  35. data/lib/aidp/execute/steps.rb +81 -82
  36. data/lib/aidp/job_manager.rb +41 -0
  37. data/lib/aidp/jobs/base_job.rb +45 -0
  38. data/lib/aidp/jobs/provider_execution_job.rb +83 -0
  39. data/lib/aidp/provider_manager.rb +25 -0
  40. data/lib/aidp/providers/agent_supervisor.rb +348 -0
  41. data/lib/aidp/providers/anthropic.rb +160 -3
  42. data/lib/aidp/providers/base.rb +153 -6
  43. data/lib/aidp/providers/cursor.rb +245 -43
  44. data/lib/aidp/providers/gemini.rb +164 -3
  45. data/lib/aidp/providers/supervised_base.rb +317 -0
  46. data/lib/aidp/providers/supervised_cursor.rb +22 -0
  47. data/lib/aidp/version.rb +1 -1
  48. data/lib/aidp.rb +31 -34
  49. data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
  50. data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
  51. metadata +91 -36
@@ -251,32 +251,27 @@ module Aidp
251
251
  def import_config(file_path, scope = :project)
252
252
  return false unless File.exist?(file_path)
253
253
 
254
- begin
255
- config_data = case File.extname(file_path)
256
- when ".yml", ".yaml"
257
- YAML.load_file(file_path)
258
- when ".json"
259
- JSON.parse(File.read(file_path))
260
- else
261
- raise ArgumentError, "Unsupported file format: #{File.extname(file_path)}"
262
- end
263
-
264
- case scope
265
- when :project
266
- @project_config = config_data
267
- save_project_config
268
- when :user
269
- @user_config = config_data
270
- save_user_config
271
- else
272
- raise ArgumentError, "Invalid scope: #{scope}"
273
- end
254
+ config_data = case File.extname(file_path)
255
+ when ".yml", ".yaml"
256
+ YAML.load_file(file_path)
257
+ when ".json"
258
+ JSON.parse(File.read(file_path))
259
+ else
260
+ raise ArgumentError, "Unsupported file format: #{File.extname(file_path)}"
261
+ end
274
262
 
275
- true
276
- rescue => e
277
- warn "Failed to import configuration: #{e.message}"
278
- false
263
+ case scope
264
+ when :project
265
+ @project_config = config_data
266
+ save_project_config
267
+ when :user
268
+ @user_config = config_data
269
+ save_user_config
270
+ else
271
+ raise ArgumentError, "Invalid scope: #{scope}"
279
272
  end
273
+
274
+ true
280
275
  end
281
276
 
282
277
  # Validate configuration
@@ -297,24 +292,14 @@ module Aidp
297
292
  def load_user_config
298
293
  return {} unless File.exist?(USER_CONFIG_FILE)
299
294
 
300
- begin
301
- YAML.load_file(USER_CONFIG_FILE) || {}
302
- rescue => e
303
- warn "Failed to load user config: #{e.message}"
304
- {}
305
- end
295
+ YAML.load_file(USER_CONFIG_FILE) || {}
306
296
  end
307
297
 
308
298
  def load_project_config
309
299
  config_path = project_config_path
310
300
  return {} unless File.exist?(config_path)
311
301
 
312
- begin
313
- YAML.load_file(config_path) || {}
314
- rescue => e
315
- warn "Failed to load project config: #{e.message}"
316
- {}
317
- end
302
+ YAML.load_file(config_path) || {}
318
303
  end
319
304
 
320
305
  def project_config_path
@@ -0,0 +1,489 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-cursor"
4
+ require "tty-screen"
5
+ require "tty-table"
6
+ require "io/console"
7
+ require "que"
8
+ require "json"
9
+ require_relative "terminal_io"
10
+
11
+ module Aidp
12
+ class CLI
13
+ class JobsCommand
14
+ def initialize(input: $stdin, output: $stdout)
15
+ @io = TerminalIO.new(input, output)
16
+ @cursor = TTY::Cursor
17
+ @screen_width = TTY::Screen.width
18
+ @screen_height = TTY::Screen.height
19
+ @running = true
20
+ @view_mode = :list
21
+ @selected_job_id = nil
22
+ @jobs_displayed = false # Track if we've displayed jobs in interactive mode
23
+ end
24
+
25
+ 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
94
+
95
+ if jobs.empty?
96
+ # Don't clear screen when no jobs - just show the message
97
+ @io.puts "Background Jobs"
98
+ @io.puts "-" * @screen_width
99
+ @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
112
+ @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"
196
+ 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
225
+ end
226
+ end
227
+
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
241
+
242
+ unless job_id
243
+ @io.print "Enter job ID: "
244
+ job_id = @io.gets.chomp
245
+ end
246
+
247
+ if job_exists?(job_id)
248
+ @selected_job_id = job_id
249
+ @view_mode = :output
250
+ end
251
+ end
252
+
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"
276
+ else
277
+ @io.puts "Job #{job_id} has no errors to retry"
278
+ end
279
+ sleep 2
280
+ end
281
+ end
282
+
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
298
+
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
310
+ end
311
+ end
312
+ end
313
+
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
+ )
338
+ 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
+
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
400
+
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
407
+ end
408
+
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
431
+
432
+ # 2. Check for any recent log entries
433
+ 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
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
+ end
475
+
476
+ def format_duration(seconds)
477
+ minutes = (seconds / 60).to_i
478
+ hours = (minutes / 60).to_i
479
+ minutes %= 60
480
+
481
+ if hours > 0
482
+ "#{hours}h #{minutes}m"
483
+ else
484
+ "#{minutes}m"
485
+ end
486
+ end
487
+ end
488
+ end
489
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Aidp
6
+ class CLI
7
+ class TerminalIO
8
+ def initialize(input = $stdin, output = $stdout)
9
+ @input = input
10
+ @output = output
11
+ end
12
+
13
+ def ready?
14
+ return false if @input.closed?
15
+ return true if @input.is_a?(StringIO)
16
+ # For regular IO, we can't easily check if data is ready
17
+ # So we'll assume it's always ready for non-blocking operations
18
+ true
19
+ end
20
+
21
+ def getch
22
+ return nil unless ready?
23
+ if @input.is_a?(StringIO)
24
+ char = @input.getc
25
+ char&.chr || ""
26
+ else
27
+ @input.getch
28
+ end
29
+ end
30
+
31
+ def gets
32
+ @input.gets
33
+ end
34
+
35
+ def write(str)
36
+ @output.write(str)
37
+ end
38
+
39
+ def puts(str = "")
40
+ @output.puts(str)
41
+ end
42
+
43
+ def print(str)
44
+ @output.print(str)
45
+ end
46
+
47
+ def flush
48
+ @output.flush
49
+ end
50
+ end
51
+ end
52
+ end