aidp 0.3.0 → 0.5.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +59 -4
  3. data/lib/aidp/analyze/agent_personas.rb +1 -1
  4. data/lib/aidp/analyze/database.rb +99 -82
  5. data/lib/aidp/analyze/error_handler.rb +12 -76
  6. data/lib/aidp/analyze/focus_guidance.rb +2 -2
  7. data/lib/aidp/analyze/metrics_storage.rb +336 -0
  8. data/lib/aidp/analyze/prioritizer.rb +2 -2
  9. data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
  10. data/lib/aidp/analyze/runner.rb +107 -191
  11. data/lib/aidp/analyze/steps.rb +29 -30
  12. data/lib/aidp/analyze/storage.rb +233 -171
  13. data/lib/aidp/cli/jobs_command.rb +489 -0
  14. data/lib/aidp/cli/terminal_io.rb +52 -0
  15. data/lib/aidp/cli.rb +104 -45
  16. data/lib/aidp/core_ext/class_attribute.rb +36 -0
  17. data/lib/aidp/database/pg_adapter.rb +148 -0
  18. data/lib/aidp/database_config.rb +69 -0
  19. data/lib/aidp/database_connection.rb +72 -0
  20. data/lib/aidp/database_migration.rb +158 -0
  21. data/lib/aidp/execute/runner.rb +65 -92
  22. data/lib/aidp/execute/steps.rb +81 -82
  23. data/lib/aidp/job_manager.rb +41 -0
  24. data/lib/aidp/jobs/base_job.rb +47 -0
  25. data/lib/aidp/jobs/provider_execution_job.rb +96 -0
  26. data/lib/aidp/provider_manager.rb +25 -0
  27. data/lib/aidp/providers/agent_supervisor.rb +348 -0
  28. data/lib/aidp/providers/anthropic.rb +166 -3
  29. data/lib/aidp/providers/base.rb +153 -6
  30. data/lib/aidp/providers/cursor.rb +247 -43
  31. data/lib/aidp/providers/gemini.rb +166 -3
  32. data/lib/aidp/providers/supervised_base.rb +317 -0
  33. data/lib/aidp/providers/supervised_cursor.rb +22 -0
  34. data/lib/aidp/version.rb +1 -1
  35. data/lib/aidp.rb +25 -34
  36. data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
  37. metadata +72 -35
@@ -27,9 +27,24 @@ module Aidp
27
27
  end
28
28
 
29
29
  # Setup logging if log file is specified
30
- log_file = ENV["AIDP_LOG_FILE"]
31
- if log_file
32
- puts "📝 Logging to: #{log_file}"
30
+ log_file = ENV["AIDP_CURSOR_LOG"]
31
+
32
+ # Smart timeout calculation
33
+ timeout_seconds = calculate_timeout
34
+
35
+ puts "⏱️ Timeout set to #{timeout_seconds} seconds"
36
+
37
+ # Set up activity monitoring
38
+ setup_activity_monitoring("cursor-agent", method(:activity_callback))
39
+ record_activity("Starting cursor-agent execution")
40
+
41
+ # Start activity display thread
42
+ activity_display_thread = Thread.new do
43
+ loop do
44
+ sleep 0.1 # Update every 100ms for smooth animation
45
+ print_activity_status
46
+ break if @activity_state == :completed || @activity_state == :failed
47
+ end
33
48
  end
34
49
 
35
50
  Open3.popen3(*cmd) do |stdin, stdout, stderr, wait|
@@ -37,62 +52,251 @@ module Aidp
37
52
  stdin.puts prompt
38
53
  stdin.close
39
54
 
40
- # Log the prompt if debugging
41
- if ENV["AIDP_DEBUG"] || log_file
42
- prompt_log = "📝 Sending prompt to cursor-agent:\n#{prompt}"
43
- puts prompt_log if ENV["AIDP_DEBUG"]
44
- File.write(log_file, "#{Time.now.iso8601} #{prompt_log}\n", mode: "a") if log_file
45
- end
55
+ # Read stdout and stderr synchronously for better reliability
56
+ output = ""
57
+ error_output = ""
46
58
 
47
- # Handle debug output and logging
48
- if ENV["AIDP_DEBUG"] || log_file
49
- # Start threads to capture and display output in real-time
50
- stdout_thread = Thread.new do
51
- stdout.each_line do |line|
52
- output = "📤 cursor-agent: #{line.chomp}"
53
- puts output if ENV["AIDP_DEBUG"]
54
- File.write(log_file, "#{Time.now.iso8601} #{output}\n", mode: "a") if log_file
59
+ # Read stdout
60
+ stdout_thread = Thread.new do
61
+ stdout&.each_line do |line|
62
+ output += line
63
+ if ENV["AIDP_DEBUG"]
64
+ clear_activity_status
65
+ puts "📤 cursor-agent: #{line.chomp}"
66
+ $stdout.flush # Force output to display immediately
55
67
  end
68
+ File.write(log_file, "#{Time.now.iso8601} #{line}\n", mode: "a") if log_file
69
+
70
+ # Record activity when we get output
71
+ record_activity("Received output: #{line.chomp[0..50]}...")
56
72
  end
73
+ rescue IOError => e
74
+ puts "📤 stdout stream closed: #{e.message}" if ENV["AIDP_DEBUG"]
75
+ end
57
76
 
58
- stderr_thread = Thread.new do
59
- stderr.each_line do |line|
60
- output = "❌ cursor-agent error: #{line.chomp}"
61
- puts output if ENV["AIDP_DEBUG"]
62
- File.write(log_file, "#{Time.now.iso8601} #{output}\n", mode: "a") if log_file
77
+ # Read stderr
78
+ stderr_thread = Thread.new do
79
+ stderr&.each_line do |line|
80
+ error_output += line
81
+ if ENV["AIDP_DEBUG"]
82
+ clear_activity_status
83
+ puts "❌ cursor-agent error: #{line.chomp}"
84
+ $stdout.flush # Force output to display immediately
63
85
  end
86
+ File.write(log_file, "#{Time.now.iso8601} #{line}\n", mode: "a") if log_file
87
+
88
+ # Record activity when we get error output
89
+ record_activity("Error output: #{line.chomp[0..50]}...")
64
90
  end
91
+ rescue IOError => e
92
+ puts "❌ stderr stream closed: #{e.message}" if ENV["AIDP_DEBUG"]
65
93
  end
66
94
 
67
- # Wait for completion with a reasonable timeout
68
- begin
69
- Timeout.timeout(300) do # 5 minutes timeout
70
- result = wait.value
95
+ # Start activity monitoring thread
96
+ activity_thread = Thread.new do
97
+ loop do
98
+ sleep 10 # Check every 10 seconds
71
99
 
72
- # Stop debug threads
73
- if ENV["AIDP_DEBUG"]
74
- stdout_thread&.kill
75
- stderr_thread&.kill
100
+ if stuck?
101
+ clear_activity_status
102
+ puts "⚠️ cursor-agent appears stuck (no activity for #{stuck_timeout} seconds)"
103
+ puts " You can:"
104
+ puts " 1. Wait longer (press Enter)"
105
+ puts " 2. Abort (Ctrl+C)"
106
+
107
+ # Give user a chance to respond
108
+ begin
109
+ Timeout.timeout(30) do
110
+ gets
111
+ puts "🔄 Continuing to wait..."
112
+ end
113
+ rescue Timeout::Error
114
+ puts "⏰ No response received, continuing to wait..."
115
+ rescue Interrupt
116
+ puts "\n🛑 User requested abort"
117
+ Process.kill("TERM", wait.pid)
118
+ break
119
+ end
76
120
  end
77
121
 
78
- return :ok if result.success?
79
- raise "cursor-agent failed with exit code #{result.exitstatus}"
122
+ # Stop checking if the process is done
123
+ break if wait.value
124
+ rescue
125
+ break
80
126
  end
81
- rescue Timeout::Error
82
- # Stop debug threads
83
- if ENV["AIDP_DEBUG"]
84
- stdout_thread&.kill
85
- stderr_thread&.kill
127
+ end
128
+
129
+ # Wait for process to complete with timeout
130
+ begin
131
+ # Start a timeout thread that will kill the process if it takes too long
132
+ timeout_thread = Thread.new do
133
+ sleep timeout_seconds
134
+ begin
135
+ Process.kill("TERM", wait.pid)
136
+ sleep 2
137
+ Process.kill("KILL", wait.pid) if wait.value.nil?
138
+ rescue
139
+ # Process already terminated
140
+ end
86
141
  end
87
142
 
88
- # Kill the process if it's taking too long
89
- begin
90
- Process.kill("TERM", wait.pid)
91
- rescue
92
- nil
143
+ # Wait for the process to complete
144
+ exit_status = wait.value
145
+
146
+ # Cancel the timeout thread since we completed successfully
147
+ timeout_thread.kill
148
+ rescue => e
149
+ # Kill the timeout thread
150
+ timeout_thread&.kill
151
+
152
+ # Check if this was a timeout
153
+ if e.is_a?(Timeout::Error) || execution_time >= timeout_seconds
154
+ # Kill the process if it times out
155
+ begin
156
+ Process.kill("TERM", wait.pid)
157
+ sleep 1
158
+ Process.kill("KILL", wait.pid) if wait.value.nil?
159
+ rescue
160
+ # Process already terminated
161
+ end
162
+
163
+ # Wait for output threads to finish (with timeout)
164
+ [stdout_thread, stderr_thread, activity_thread].each do |thread|
165
+ thread.join(5) # Wait up to 5 seconds for each thread
166
+ end
167
+
168
+ # Stop activity display
169
+ activity_display_thread.join
170
+
171
+ clear_activity_status
172
+ mark_failed("cursor-agent timed out after #{timeout_seconds} seconds")
173
+ raise Timeout::Error, "cursor-agent timed out after #{timeout_seconds} seconds"
174
+ else
175
+ raise e
93
176
  end
94
- raise Timeout::Error, "cursor-agent timed out after 5 minutes"
95
177
  end
178
+
179
+ # Wait for output threads to finish (with timeout)
180
+ [stdout_thread, stderr_thread, activity_thread].each do |thread|
181
+ thread.join(5) # Wait up to 5 seconds for each thread
182
+ end
183
+
184
+ # Stop activity display
185
+ activity_display_thread.join
186
+
187
+ clear_activity_status
188
+ if exit_status.success?
189
+ mark_completed
190
+ output
191
+ else
192
+ mark_failed("cursor-agent failed with exit code #{exit_status.exitstatus}")
193
+ raise "cursor-agent failed with exit code #{exit_status.exitstatus}: #{error_output}"
194
+ end
195
+ end
196
+ rescue Timeout::Error
197
+ clear_activity_status
198
+ mark_failed("cursor-agent timed out after #{timeout_seconds} seconds")
199
+ raise Timeout::Error, "cursor-agent timed out after #{timeout_seconds} seconds"
200
+ rescue => e
201
+ clear_activity_status
202
+ mark_failed("cursor-agent execution was interrupted: #{e.message}")
203
+ raise
204
+ end
205
+
206
+ private
207
+
208
+ def print_activity_status
209
+ # Print activity status during cursor execution
210
+ print "🔄 cursor-agent is running..."
211
+ $stdout.flush
212
+ end
213
+
214
+ def clear_activity_status
215
+ # Clear the activity status line
216
+ print "\r" + " " * 50 + "\r"
217
+ $stdout.flush
218
+ end
219
+
220
+ def calculate_timeout
221
+ # Priority order for timeout calculation:
222
+ # 1. Quick mode (for testing)
223
+ # 2. Environment variable override
224
+ # 3. Adaptive timeout based on step type
225
+ # 4. Default timeout
226
+
227
+ if ENV["AIDP_QUICK_MODE"]
228
+ puts "⚡ Quick mode enabled - 2 minute timeout"
229
+ return 120
230
+ end
231
+
232
+ if ENV["AIDP_CURSOR_TIMEOUT"]
233
+ return ENV["AIDP_CURSOR_TIMEOUT"].to_i
234
+ end
235
+
236
+ # Adaptive timeout based on step type
237
+ step_timeout = get_adaptive_timeout
238
+ if step_timeout
239
+ puts "🧠 Using adaptive timeout: #{step_timeout} seconds"
240
+ return step_timeout
241
+ end
242
+
243
+ # Default timeout (5 minutes for interactive use)
244
+ puts "📋 Using default timeout: 5 minutes"
245
+ 300
246
+ end
247
+
248
+ def get_adaptive_timeout
249
+ # Try to get timeout recommendations from metrics storage
250
+ begin
251
+ require_relative "../analyze/metrics_storage"
252
+ storage = Aidp::Analyze::MetricsStorage.new(Dir.pwd)
253
+ recommendations = storage.calculate_timeout_recommendations
254
+
255
+ # Get current step name from environment or context
256
+ step_name = ENV["AIDP_CURRENT_STEP"] || "unknown"
257
+
258
+ if recommendations[step_name]
259
+ recommended = recommendations[step_name][:recommended_timeout]
260
+ # Add 20% buffer for safety
261
+ return (recommended * 1.2).ceil
262
+ end
263
+ rescue => e
264
+ puts "⚠️ Could not get adaptive timeout: #{e.message}" if ENV["AIDP_DEBUG"]
265
+ end
266
+
267
+ # Fallback timeouts based on step type patterns
268
+ step_name = ENV["AIDP_CURRENT_STEP"] || ""
269
+
270
+ case step_name
271
+ when /REPOSITORY_ANALYSIS/
272
+ 180 # 3 minutes - repository analysis can be quick
273
+ when /ARCHITECTURE_ANALYSIS/
274
+ 600 # 10 minutes - architecture analysis needs more time
275
+ when /TEST_ANALYSIS/
276
+ 300 # 5 minutes - test analysis is moderate
277
+ when /FUNCTIONALITY_ANALYSIS/
278
+ 600 # 10 minutes - functionality analysis is complex
279
+ when /DOCUMENTATION_ANALYSIS/
280
+ 300 # 5 minutes - documentation analysis is moderate
281
+ when /STATIC_ANALYSIS/
282
+ 450 # 7.5 minutes - static analysis can be intensive
283
+ when /REFACTORING_RECOMMENDATIONS/
284
+ 600 # 10 minutes - refactoring recommendations are complex
285
+ else
286
+ nil # Use default
287
+ end
288
+ end
289
+
290
+ def activity_callback(state, message, provider)
291
+ # This is now handled by the animated display thread
292
+ # Only print static messages for state changes
293
+ case state
294
+ when :stuck
295
+ puts "\n⚠️ cursor appears stuck: #{message}"
296
+ when :completed
297
+ puts "\n✅ cursor completed: #{message}"
298
+ when :failed
299
+ puts "\n❌ cursor failed: #{message}"
96
300
  end
97
301
  end
98
302
  end
@@ -14,10 +14,173 @@ module Aidp
14
14
  def send(prompt:, session: nil)
15
15
  raise "gemini CLI not available" unless self.class.available?
16
16
 
17
+ require "open3"
18
+
17
19
  # Use Gemini CLI for non-interactive mode
18
- cmd = ["gemini", "chat", "--prompt", prompt]
19
- system(*cmd)
20
- :ok
20
+ cmd = ["gemini", "--print"]
21
+
22
+ puts "📝 Sending prompt to gemini..."
23
+
24
+ # Smart timeout calculation
25
+ timeout_seconds = calculate_timeout
26
+
27
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait|
28
+ # Send the prompt to stdin
29
+ stdin.puts prompt
30
+ stdin.close
31
+
32
+ # Start stuck detection thread
33
+ stuck_detection_thread = Thread.new do
34
+ loop do
35
+ sleep 10 # Check every 10 seconds
36
+
37
+ if stuck?
38
+ puts "⚠️ gemini appears stuck (no activity for #{stuck_timeout} seconds)"
39
+ puts " You can:"
40
+ puts " 1. Wait longer (press Enter)"
41
+ puts " 2. Abort (Ctrl+C)"
42
+
43
+ # Give user a chance to respond
44
+ begin
45
+ Timeout.timeout(30) do
46
+ gets
47
+ puts "🔄 Continuing to wait..."
48
+ end
49
+ rescue Timeout::Error
50
+ puts "⏰ No response received, continuing to wait..."
51
+ rescue Interrupt
52
+ puts "🛑 Aborting gemini..."
53
+ Process.kill("TERM", wait.pid)
54
+ raise Interrupt, "User aborted gemini execution"
55
+ end
56
+ end
57
+
58
+ # Stop checking if the process is done
59
+ break if wait.value
60
+ rescue
61
+ break
62
+ end
63
+ end
64
+
65
+ # Wait for completion with timeout
66
+ begin
67
+ Timeout.timeout(timeout_seconds) do
68
+ result = wait.value
69
+
70
+ # Stop stuck detection thread
71
+ stuck_detection_thread&.kill
72
+
73
+ if result.success?
74
+ output = stdout.read
75
+ puts "✅ Gemini analysis completed"
76
+ mark_completed
77
+ return output.empty? ? :ok : output
78
+ else
79
+ error_output = stderr.read
80
+ mark_failed("gemini failed with exit code #{result.exitstatus}: #{error_output}")
81
+ raise "gemini failed with exit code #{result.exitstatus}: #{error_output}"
82
+ end
83
+ end
84
+ rescue Timeout::Error
85
+ # Stop stuck detection thread
86
+ stuck_detection_thread&.kill
87
+
88
+ # Kill the process if it's taking too long
89
+ begin
90
+ Process.kill("TERM", wait.pid)
91
+ rescue
92
+ nil
93
+ end
94
+
95
+ mark_failed("gemini timed out after #{timeout_seconds} seconds")
96
+ raise Timeout::Error, "gemini timed out after #{timeout_seconds} seconds"
97
+ rescue Interrupt
98
+ # Stop stuck detection thread
99
+ stuck_detection_thread&.kill
100
+
101
+ # Kill the process
102
+ begin
103
+ Process.kill("TERM", wait.pid)
104
+ rescue
105
+ nil
106
+ end
107
+
108
+ mark_failed("gemini execution was interrupted")
109
+ raise
110
+ end
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ def calculate_timeout
117
+ # Priority order for timeout calculation:
118
+ # 1. Quick mode (for testing)
119
+ # 2. Environment variable override
120
+ # 3. Adaptive timeout based on step type
121
+ # 4. Default timeout
122
+
123
+ if ENV["AIDP_QUICK_MODE"]
124
+ puts "⚡ Quick mode enabled - 2 minute timeout"
125
+ return 120
126
+ end
127
+
128
+ if ENV["AIDP_GEMINI_TIMEOUT"]
129
+ return ENV["AIDP_GEMINI_TIMEOUT"].to_i
130
+ end
131
+
132
+ # Adaptive timeout based on step type
133
+ step_timeout = get_adaptive_timeout
134
+ if step_timeout
135
+ puts "🧠 Using adaptive timeout: #{step_timeout} seconds"
136
+ return step_timeout
137
+ end
138
+
139
+ # Default timeout (5 minutes for interactive use)
140
+ puts "📋 Using default timeout: 5 minutes"
141
+ 300
142
+ end
143
+
144
+ def get_adaptive_timeout
145
+ # Try to get timeout recommendations from metrics storage
146
+ begin
147
+ require_relative "../analyze/metrics_storage"
148
+ storage = Aidp::Analyze::MetricsStorage.new(Dir.pwd)
149
+ recommendations = storage.calculate_timeout_recommendations
150
+
151
+ # Get current step name from environment or context
152
+ step_name = ENV["AIDP_CURRENT_STEP"] || "unknown"
153
+
154
+ if recommendations[step_name]
155
+ recommended = recommendations[step_name][:recommended_timeout]
156
+ # Add 20% buffer for safety
157
+ return (recommended * 1.2).ceil
158
+ end
159
+ rescue => e
160
+ puts "⚠️ Could not get adaptive timeout: #{e.message}" if ENV["AIDP_DEBUG"]
161
+ end
162
+
163
+ # Fallback timeouts based on step type patterns
164
+ step_name = ENV["AIDP_CURRENT_STEP"] || ""
165
+
166
+ case step_name
167
+ when /REPOSITORY_ANALYSIS/
168
+ 180 # 3 minutes - repository analysis can be quick
169
+ when /ARCHITECTURE_ANALYSIS/
170
+ 600 # 10 minutes - architecture analysis needs more time
171
+ when /TEST_ANALYSIS/
172
+ 300 # 5 minutes - test analysis is moderate
173
+ when /FUNCTIONALITY_ANALYSIS/
174
+ 600 # 10 minutes - functionality analysis is complex
175
+ when /DOCUMENTATION_ANALYSIS/
176
+ 300 # 5 minutes - documentation analysis is moderate
177
+ when /STATIC_ANALYSIS/
178
+ 450 # 7.5 minutes - static analysis can be intensive
179
+ when /REFACTORING_RECOMMENDATIONS/
180
+ 600 # 10 minutes - refactoring recommendations are complex
181
+ else
182
+ nil # Use default
183
+ end
21
184
  end
22
185
  end
23
186
  end