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.
- checksums.yaml +4 -4
- data/README.md +191 -5
- data/lib/aidp/analysis/kb_inspector.rb +456 -0
- data/lib/aidp/analysis/seams.rb +188 -0
- data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +493 -0
- data/lib/aidp/analysis/tree_sitter_scan.rb +703 -0
- data/lib/aidp/analyze/agent_personas.rb +1 -1
- data/lib/aidp/analyze/agent_tool_executor.rb +5 -11
- data/lib/aidp/analyze/data_retention_manager.rb +0 -5
- data/lib/aidp/analyze/database.rb +99 -82
- data/lib/aidp/analyze/error_handler.rb +12 -79
- data/lib/aidp/analyze/export_manager.rb +0 -7
- data/lib/aidp/analyze/focus_guidance.rb +2 -2
- data/lib/aidp/analyze/incremental_analyzer.rb +1 -11
- data/lib/aidp/analyze/large_analysis_progress.rb +0 -5
- data/lib/aidp/analyze/memory_manager.rb +34 -60
- data/lib/aidp/analyze/metrics_storage.rb +336 -0
- data/lib/aidp/analyze/parallel_processor.rb +0 -6
- data/lib/aidp/analyze/performance_optimizer.rb +0 -3
- data/lib/aidp/analyze/prioritizer.rb +2 -2
- data/lib/aidp/analyze/repository_chunker.rb +14 -21
- data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
- data/lib/aidp/analyze/runner.rb +107 -191
- data/lib/aidp/analyze/steps.rb +35 -30
- data/lib/aidp/analyze/storage.rb +233 -178
- data/lib/aidp/analyze/tool_configuration.rb +21 -36
- data/lib/aidp/cli/jobs_command.rb +489 -0
- data/lib/aidp/cli/terminal_io.rb +52 -0
- data/lib/aidp/cli.rb +160 -45
- data/lib/aidp/core_ext/class_attribute.rb +36 -0
- data/lib/aidp/database/pg_adapter.rb +148 -0
- data/lib/aidp/database_config.rb +69 -0
- data/lib/aidp/database_connection.rb +72 -0
- data/lib/aidp/execute/runner.rb +65 -92
- data/lib/aidp/execute/steps.rb +81 -82
- data/lib/aidp/job_manager.rb +41 -0
- data/lib/aidp/jobs/base_job.rb +45 -0
- data/lib/aidp/jobs/provider_execution_job.rb +83 -0
- data/lib/aidp/provider_manager.rb +25 -0
- data/lib/aidp/providers/agent_supervisor.rb +348 -0
- data/lib/aidp/providers/anthropic.rb +160 -3
- data/lib/aidp/providers/base.rb +153 -6
- data/lib/aidp/providers/cursor.rb +245 -43
- data/lib/aidp/providers/gemini.rb +164 -3
- data/lib/aidp/providers/supervised_base.rb +317 -0
- data/lib/aidp/providers/supervised_cursor.rb +22 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +31 -34
- data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
- data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
- metadata +91 -36
@@ -0,0 +1,348 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
require "open3"
|
5
|
+
|
6
|
+
module Aidp
|
7
|
+
module Providers
|
8
|
+
# Supervisor for managing agent execution with progressive warnings instead of hard timeouts
|
9
|
+
class AgentSupervisor
|
10
|
+
# Execution states
|
11
|
+
STATES = {
|
12
|
+
idle: "⏳",
|
13
|
+
starting: "🚀",
|
14
|
+
running: "🔄",
|
15
|
+
warning: "⚠️",
|
16
|
+
user_aborted: "🛑",
|
17
|
+
completed: "✅",
|
18
|
+
failed: "❌"
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
attr_reader :state, :start_time, :end_time, :duration, :output, :error_output, :exit_code
|
22
|
+
|
23
|
+
def initialize(command, timeout_seconds: 300, debug: false)
|
24
|
+
@command = command
|
25
|
+
@timeout_seconds = timeout_seconds
|
26
|
+
@debug = debug
|
27
|
+
@state = :idle
|
28
|
+
@start_time = nil
|
29
|
+
@end_time = nil
|
30
|
+
@duration = 0
|
31
|
+
@output = ""
|
32
|
+
@error_output = ""
|
33
|
+
@exit_code = nil
|
34
|
+
@process_pid = nil
|
35
|
+
@output_count = 0
|
36
|
+
@last_output_time = nil
|
37
|
+
@supervisor_thread = nil
|
38
|
+
@output_threads = []
|
39
|
+
@warning_shown = false
|
40
|
+
@user_aborted = false
|
41
|
+
end
|
42
|
+
|
43
|
+
# Execute the command with supervision
|
44
|
+
def execute(input = nil)
|
45
|
+
@state = :starting
|
46
|
+
@start_time = Time.now
|
47
|
+
@last_output_time = @start_time
|
48
|
+
|
49
|
+
puts "🚀 Starting agent execution (will warn at #{@timeout_seconds}s)"
|
50
|
+
|
51
|
+
begin
|
52
|
+
# Start the process
|
53
|
+
Open3.popen3(*@command) do |stdin, stdout, stderr, wait|
|
54
|
+
@process_pid = wait.pid
|
55
|
+
@state = :running
|
56
|
+
|
57
|
+
# Send input if provided
|
58
|
+
if input
|
59
|
+
stdin.puts input
|
60
|
+
stdin.close
|
61
|
+
end
|
62
|
+
|
63
|
+
# Start timeout thread that will warn but not kill
|
64
|
+
timeout_thread = Thread.new do
|
65
|
+
sleep @timeout_seconds
|
66
|
+
if @state == :running && !@warning_shown
|
67
|
+
show_timeout_warning
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Start supervisor thread
|
72
|
+
start_supervisor_thread(wait)
|
73
|
+
|
74
|
+
# Start output collection threads
|
75
|
+
start_output_threads(stdout, stderr)
|
76
|
+
|
77
|
+
# Wait for completion
|
78
|
+
result = wait_for_completion(wait)
|
79
|
+
|
80
|
+
# Kill timeout thread since we're done
|
81
|
+
timeout_thread.kill
|
82
|
+
|
83
|
+
# Clean up threads
|
84
|
+
cleanup_threads
|
85
|
+
|
86
|
+
@end_time = Time.now
|
87
|
+
@duration = @end_time - @start_time
|
88
|
+
|
89
|
+
return result
|
90
|
+
end
|
91
|
+
rescue => e
|
92
|
+
@state = :failed
|
93
|
+
@end_time = Time.now
|
94
|
+
@duration = @end_time - @start_time if @start_time
|
95
|
+
puts "❌ Agent execution failed: #{e.message}"
|
96
|
+
raise
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Get current execution status
|
101
|
+
def status
|
102
|
+
elapsed = @start_time ? Time.now - @start_time : 0
|
103
|
+
minutes = (elapsed / 60).to_i
|
104
|
+
seconds = (elapsed % 60).to_i
|
105
|
+
time_str = (minutes > 0) ? "#{minutes}m #{seconds}s" : "#{seconds}s"
|
106
|
+
|
107
|
+
case @state
|
108
|
+
when :idle
|
109
|
+
"⏳ Idle"
|
110
|
+
when :starting
|
111
|
+
"🚀 Starting..."
|
112
|
+
when :running
|
113
|
+
output_info = (@output_count > 0) ? " (#{@output_count} outputs)" : ""
|
114
|
+
"🔄 Running #{time_str}#{output_info}"
|
115
|
+
when :warning
|
116
|
+
"⚠️ Taking longer than expected #{time_str}"
|
117
|
+
when :user_aborted
|
118
|
+
"🛑 Aborted by user after #{time_str}"
|
119
|
+
when :completed
|
120
|
+
"✅ Completed in #{time_str}"
|
121
|
+
when :failed
|
122
|
+
"❌ Failed after #{time_str}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Check if execution is still active
|
127
|
+
def active?
|
128
|
+
[:starting, :running, :warning].include?(@state)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Check if execution completed successfully
|
132
|
+
def success?
|
133
|
+
@state == :completed && @exit_code == 0
|
134
|
+
end
|
135
|
+
|
136
|
+
# Show timeout warning and give user control
|
137
|
+
def show_timeout_warning
|
138
|
+
return if @warning_shown
|
139
|
+
@warning_shown = true
|
140
|
+
@state = :warning
|
141
|
+
|
142
|
+
puts "\n⚠️ Agent has been running for #{@timeout_seconds} seconds"
|
143
|
+
puts " This is longer than expected, but the agent may still be working."
|
144
|
+
puts " You can:"
|
145
|
+
puts " 1. Continue waiting (press Enter)"
|
146
|
+
puts " 2. Abort execution (type 'abort' and press Enter)"
|
147
|
+
puts " 3. Wait 5 more minutes (type 'wait' and press Enter)"
|
148
|
+
|
149
|
+
begin
|
150
|
+
Timeout.timeout(30) do
|
151
|
+
response = gets&.chomp&.downcase
|
152
|
+
case response
|
153
|
+
when "abort"
|
154
|
+
puts "🛑 Aborting execution..."
|
155
|
+
@user_aborted = true
|
156
|
+
@state = :user_aborted
|
157
|
+
kill!
|
158
|
+
when "wait"
|
159
|
+
puts "⏰ Will warn again in 5 minutes..."
|
160
|
+
@warning_shown = false
|
161
|
+
@state = :running
|
162
|
+
# Start another warning thread for 5 more minutes
|
163
|
+
Thread.new do
|
164
|
+
sleep 300 # 5 minutes
|
165
|
+
if @state == :running && !@warning_shown
|
166
|
+
show_timeout_warning
|
167
|
+
end
|
168
|
+
end
|
169
|
+
else
|
170
|
+
puts "🔄 Continuing to wait..."
|
171
|
+
@state = :running
|
172
|
+
end
|
173
|
+
end
|
174
|
+
rescue Timeout::Error
|
175
|
+
puts "⏰ No response received, continuing to wait..."
|
176
|
+
@state = :running
|
177
|
+
rescue Interrupt
|
178
|
+
puts "\n🛑 User interrupted, aborting..."
|
179
|
+
@user_aborted = true
|
180
|
+
@state = :user_aborted
|
181
|
+
kill!
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Force kill the process
|
186
|
+
def kill!
|
187
|
+
return unless @process_pid && active?
|
188
|
+
|
189
|
+
puts "💀 Force killing agent process (PID: #{@process_pid})"
|
190
|
+
|
191
|
+
begin
|
192
|
+
# Try graceful termination first
|
193
|
+
Process.kill("TERM", @process_pid)
|
194
|
+
sleep 1
|
195
|
+
|
196
|
+
# Force kill if still running
|
197
|
+
if process_running?(@process_pid)
|
198
|
+
Process.kill("KILL", @process_pid)
|
199
|
+
sleep 1
|
200
|
+
end
|
201
|
+
|
202
|
+
# Double-check and force kill again if needed
|
203
|
+
if process_running?(@process_pid)
|
204
|
+
puts "⚠️ Process still running, using SIGKILL..."
|
205
|
+
Process.kill("KILL", @process_pid)
|
206
|
+
sleep 1
|
207
|
+
end
|
208
|
+
|
209
|
+
@state = :user_aborted
|
210
|
+
rescue Errno::ESRCH
|
211
|
+
# Process already dead
|
212
|
+
@state = :user_aborted
|
213
|
+
rescue => e
|
214
|
+
puts "⚠️ Error killing process: #{e.message}"
|
215
|
+
# Try one more time with KILL
|
216
|
+
begin
|
217
|
+
Process.kill("KILL", @process_pid) if process_running?(@process_pid)
|
218
|
+
rescue Errno::ESRCH
|
219
|
+
# Process already terminated
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
def start_supervisor_thread(wait)
|
227
|
+
@supervisor_thread = Thread.new do
|
228
|
+
loop do
|
229
|
+
sleep 10 # Check every 10 seconds
|
230
|
+
|
231
|
+
# Check if process is done
|
232
|
+
if wait.value
|
233
|
+
break
|
234
|
+
end
|
235
|
+
|
236
|
+
# Check for stuck condition (no output for 3 minutes)
|
237
|
+
if @last_output_time && Time.now - @last_output_time > 180
|
238
|
+
puts "⚠️ Agent appears stuck (no output for 3+ minutes)"
|
239
|
+
# Don't kill, just warn
|
240
|
+
end
|
241
|
+
end
|
242
|
+
rescue => e
|
243
|
+
puts "⚠️ Supervisor thread error: #{e.message}" if @debug
|
244
|
+
end
|
245
|
+
|
246
|
+
@supervisor_thread
|
247
|
+
end
|
248
|
+
|
249
|
+
def start_output_threads(stdout, stderr)
|
250
|
+
# Stdout thread
|
251
|
+
@output_threads << Thread.new do
|
252
|
+
stdout.each_line do |line|
|
253
|
+
@output += line
|
254
|
+
@output_count += 1
|
255
|
+
@last_output_time = Time.now
|
256
|
+
|
257
|
+
if @debug
|
258
|
+
puts "📤 #{line.chomp}"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
rescue IOError => e
|
262
|
+
puts "📤 stdout closed: #{e.message}" if @debug
|
263
|
+
rescue => e
|
264
|
+
puts "⚠️ stdout thread error: #{e.message}" if @debug
|
265
|
+
end
|
266
|
+
|
267
|
+
# Stderr thread
|
268
|
+
@output_threads << Thread.new do
|
269
|
+
stderr.each_line do |line|
|
270
|
+
@error_output += line
|
271
|
+
@output_count += 1
|
272
|
+
@last_output_time = Time.now
|
273
|
+
|
274
|
+
if @debug
|
275
|
+
puts "❌ #{line.chomp}"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
rescue IOError => e
|
279
|
+
puts "❌ stderr closed: #{e.message}" if @debug
|
280
|
+
rescue => e
|
281
|
+
puts "⚠️ stderr thread error: #{e.message}" if @debug
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def wait_for_completion(wait)
|
286
|
+
# Wait for process to complete
|
287
|
+
exit_status = wait.value
|
288
|
+
@exit_code = exit_status.exitstatus
|
289
|
+
|
290
|
+
# Update duration
|
291
|
+
@duration = Time.now - @start_time
|
292
|
+
|
293
|
+
if @user_aborted || @state == :user_aborted
|
294
|
+
# Process was killed by user
|
295
|
+
{
|
296
|
+
success: false,
|
297
|
+
state: @state,
|
298
|
+
output: @output,
|
299
|
+
error_output: @error_output,
|
300
|
+
exit_code: @exit_code,
|
301
|
+
duration: @duration,
|
302
|
+
reason: "user_aborted"
|
303
|
+
}
|
304
|
+
elsif exit_status.success?
|
305
|
+
@state = :completed
|
306
|
+
{
|
307
|
+
success: true,
|
308
|
+
state: @state,
|
309
|
+
output: @output,
|
310
|
+
error_output: @error_output,
|
311
|
+
exit_code: @exit_code,
|
312
|
+
duration: @duration
|
313
|
+
}
|
314
|
+
else
|
315
|
+
@state = :failed
|
316
|
+
{
|
317
|
+
success: false,
|
318
|
+
state: @state,
|
319
|
+
output: @output,
|
320
|
+
error_output: @error_output,
|
321
|
+
exit_code: @exit_code,
|
322
|
+
duration: @duration,
|
323
|
+
reason: "non_zero_exit"
|
324
|
+
}
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
def cleanup_threads
|
329
|
+
# Wait for output threads to finish (with timeout)
|
330
|
+
@output_threads.each do |thread|
|
331
|
+
thread.join(5) # Wait up to 5 seconds
|
332
|
+
rescue => e
|
333
|
+
puts "⚠️ Error joining thread: #{e.message}" if @debug
|
334
|
+
end
|
335
|
+
|
336
|
+
# Kill supervisor thread
|
337
|
+
@supervisor_thread&.kill
|
338
|
+
end
|
339
|
+
|
340
|
+
def process_running?(pid)
|
341
|
+
Process.kill(0, pid)
|
342
|
+
true
|
343
|
+
rescue Errno::ESRCH
|
344
|
+
false
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
@@ -14,10 +14,167 @@ module Aidp
|
|
14
14
|
def send(prompt:, session: nil)
|
15
15
|
raise "claude CLI not available" unless self.class.available?
|
16
16
|
|
17
|
+
require "open3"
|
18
|
+
|
17
19
|
# Use Claude CLI for non-interactive mode
|
18
|
-
cmd = ["claude", "
|
19
|
-
|
20
|
-
|
20
|
+
cmd = ["claude", "--print"]
|
21
|
+
|
22
|
+
puts "📝 Sending prompt to claude..."
|
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 "⚠️ claude 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 claude..."
|
53
|
+
Process.kill("TERM", wait.pid)
|
54
|
+
raise Interrupt, "User aborted claude execution"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Stop checking if the process is done
|
59
|
+
break if wait.value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Wait for completion with timeout
|
64
|
+
begin
|
65
|
+
Timeout.timeout(timeout_seconds) do
|
66
|
+
result = wait.value
|
67
|
+
|
68
|
+
# Stop stuck detection thread
|
69
|
+
stuck_detection_thread&.kill
|
70
|
+
|
71
|
+
if result.success?
|
72
|
+
output = stdout.read
|
73
|
+
puts "✅ Claude analysis completed"
|
74
|
+
mark_completed
|
75
|
+
return output.empty? ? :ok : output
|
76
|
+
else
|
77
|
+
error_output = stderr.read
|
78
|
+
mark_failed("claude failed with exit code #{result.exitstatus}: #{error_output}")
|
79
|
+
raise "claude failed with exit code #{result.exitstatus}: #{error_output}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
rescue Timeout::Error
|
83
|
+
# Stop stuck detection thread
|
84
|
+
stuck_detection_thread&.kill
|
85
|
+
|
86
|
+
# Kill the process if it's taking too long
|
87
|
+
begin
|
88
|
+
Process.kill("TERM", wait.pid)
|
89
|
+
rescue Errno::ESRCH
|
90
|
+
# Process already terminated
|
91
|
+
end
|
92
|
+
|
93
|
+
mark_failed("claude timed out after #{timeout_seconds} seconds")
|
94
|
+
raise Timeout::Error, "claude timed out after #{timeout_seconds} seconds"
|
95
|
+
rescue Interrupt
|
96
|
+
# Stop stuck detection thread
|
97
|
+
stuck_detection_thread&.kill
|
98
|
+
|
99
|
+
# Kill the process
|
100
|
+
begin
|
101
|
+
Process.kill("TERM", wait.pid)
|
102
|
+
rescue Errno::ESRCH
|
103
|
+
# Process already terminated
|
104
|
+
end
|
105
|
+
|
106
|
+
mark_failed("claude execution was interrupted")
|
107
|
+
raise
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def calculate_timeout
|
115
|
+
# Priority order for timeout calculation:
|
116
|
+
# 1. Quick mode (for testing)
|
117
|
+
# 2. Environment variable override
|
118
|
+
# 3. Adaptive timeout based on step type
|
119
|
+
# 4. Default timeout
|
120
|
+
|
121
|
+
if ENV["AIDP_QUICK_MODE"]
|
122
|
+
puts "⚡ Quick mode enabled - 2 minute timeout"
|
123
|
+
return 120
|
124
|
+
end
|
125
|
+
|
126
|
+
if ENV["AIDP_ANTHROPIC_TIMEOUT"]
|
127
|
+
return ENV["AIDP_ANTHROPIC_TIMEOUT"].to_i
|
128
|
+
end
|
129
|
+
|
130
|
+
# Adaptive timeout based on step type
|
131
|
+
step_timeout = get_adaptive_timeout
|
132
|
+
if step_timeout
|
133
|
+
puts "🧠 Using adaptive timeout: #{step_timeout} seconds"
|
134
|
+
return step_timeout
|
135
|
+
end
|
136
|
+
|
137
|
+
# Default timeout (5 minutes for interactive use)
|
138
|
+
puts "📋 Using default timeout: 5 minutes"
|
139
|
+
300
|
140
|
+
end
|
141
|
+
|
142
|
+
def get_adaptive_timeout
|
143
|
+
# Try to get timeout recommendations from metrics storage
|
144
|
+
require_relative "../analyze/metrics_storage"
|
145
|
+
storage = Aidp::Analyze::MetricsStorage.new(Dir.pwd)
|
146
|
+
recommendations = storage.calculate_timeout_recommendations
|
147
|
+
|
148
|
+
# Get current step name from environment or context
|
149
|
+
step_name = ENV["AIDP_CURRENT_STEP"] || "unknown"
|
150
|
+
|
151
|
+
if recommendations[step_name]
|
152
|
+
recommended = recommendations[step_name][:recommended_timeout]
|
153
|
+
# Add 20% buffer for safety
|
154
|
+
return (recommended * 1.2).ceil
|
155
|
+
end
|
156
|
+
|
157
|
+
# Fallback timeouts based on step type patterns
|
158
|
+
step_name = ENV["AIDP_CURRENT_STEP"] || ""
|
159
|
+
|
160
|
+
case step_name
|
161
|
+
when /REPOSITORY_ANALYSIS/
|
162
|
+
180 # 3 minutes - repository analysis can be quick
|
163
|
+
when /ARCHITECTURE_ANALYSIS/
|
164
|
+
600 # 10 minutes - architecture analysis needs more time
|
165
|
+
when /TEST_ANALYSIS/
|
166
|
+
300 # 5 minutes - test analysis is moderate
|
167
|
+
when /FUNCTIONALITY_ANALYSIS/
|
168
|
+
600 # 10 minutes - functionality analysis is complex
|
169
|
+
when /DOCUMENTATION_ANALYSIS/
|
170
|
+
300 # 5 minutes - documentation analysis is moderate
|
171
|
+
when /STATIC_ANALYSIS/
|
172
|
+
450 # 7.5 minutes - static analysis can be intensive
|
173
|
+
when /REFACTORING_RECOMMENDATIONS/
|
174
|
+
600 # 10 minutes - refactoring recommendations are complex
|
175
|
+
else
|
176
|
+
nil # Use default
|
177
|
+
end
|
21
178
|
end
|
22
179
|
end
|
23
180
|
end
|
data/lib/aidp/providers/base.rb
CHANGED
@@ -3,13 +3,160 @@
|
|
3
3
|
module Aidp
|
4
4
|
module Providers
|
5
5
|
class Base
|
6
|
-
|
6
|
+
# Activity indicator states
|
7
|
+
ACTIVITY_STATES = {
|
8
|
+
idle: "⏳",
|
9
|
+
working: "🔄",
|
10
|
+
stuck: "⚠️",
|
11
|
+
completed: "✅",
|
12
|
+
failed: "❌"
|
13
|
+
}.freeze
|
7
14
|
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
15
|
+
# Default timeout for stuck detection (2 minutes)
|
16
|
+
DEFAULT_STUCK_TIMEOUT = 120
|
17
|
+
|
18
|
+
attr_reader :activity_state, :last_activity_time, :start_time, :step_name
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@activity_state = :idle
|
22
|
+
@last_activity_time = Time.now
|
23
|
+
@start_time = nil
|
24
|
+
@step_name = nil
|
25
|
+
@activity_callback = nil
|
26
|
+
@stuck_timeout = DEFAULT_STUCK_TIMEOUT
|
27
|
+
@output_count = 0
|
28
|
+
@last_output_time = Time.now
|
29
|
+
@job_context = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def name
|
33
|
+
raise NotImplementedError, "#{self.class} must implement #name"
|
34
|
+
end
|
35
|
+
|
36
|
+
def send(prompt:, session: nil)
|
37
|
+
raise NotImplementedError, "#{self.class} must implement #send"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Set job context for background execution
|
41
|
+
def set_job_context(job_id:, execution_id:, job_manager:)
|
42
|
+
@job_context = {
|
43
|
+
job_id: job_id,
|
44
|
+
execution_id: execution_id,
|
45
|
+
job_manager: job_manager
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set up activity monitoring for a step
|
50
|
+
def setup_activity_monitoring(step_name, activity_callback = nil, stuck_timeout = nil)
|
51
|
+
@step_name = step_name
|
52
|
+
@activity_callback = activity_callback
|
53
|
+
@stuck_timeout = stuck_timeout || DEFAULT_STUCK_TIMEOUT
|
54
|
+
@start_time = Time.now
|
55
|
+
@last_activity_time = @start_time
|
56
|
+
@output_count = 0
|
57
|
+
@last_output_time = @start_time
|
58
|
+
update_activity_state(:working)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Update activity state and notify callback
|
62
|
+
def update_activity_state(state, message = nil)
|
63
|
+
@activity_state = state
|
64
|
+
@last_activity_time = Time.now if state == :working
|
65
|
+
|
66
|
+
# Log state change to job if in background mode
|
67
|
+
if @job_context
|
68
|
+
level = case state
|
69
|
+
when :completed then "info"
|
70
|
+
when :failed then "error"
|
71
|
+
else "debug"
|
72
|
+
end
|
73
|
+
|
74
|
+
log_to_job(message || "Provider state changed to #{state}", level)
|
75
|
+
end
|
76
|
+
|
77
|
+
@activity_callback&.call(state, message, self)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Check if provider appears to be stuck
|
81
|
+
def stuck?
|
82
|
+
return false unless @activity_state == :working
|
83
|
+
|
84
|
+
time_since_activity = Time.now - @last_activity_time
|
85
|
+
time_since_activity > @stuck_timeout
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get current execution time
|
89
|
+
def execution_time
|
90
|
+
return 0 unless @start_time
|
91
|
+
Time.now - @start_time
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get time since last activity
|
95
|
+
def time_since_last_activity
|
96
|
+
Time.now - @last_activity_time
|
97
|
+
end
|
98
|
+
|
99
|
+
# Record activity (called when provider produces output)
|
100
|
+
def record_activity(message = nil)
|
101
|
+
@output_count += 1
|
102
|
+
@last_output_time = Time.now
|
103
|
+
update_activity_state(:working, message)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Mark as completed
|
107
|
+
def mark_completed
|
108
|
+
update_activity_state(:completed)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Mark as failed
|
112
|
+
def mark_failed(error_message = nil)
|
113
|
+
update_activity_state(:failed, error_message)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get activity summary for metrics
|
117
|
+
def activity_summary
|
118
|
+
{
|
119
|
+
provider: name,
|
120
|
+
step_name: @step_name,
|
121
|
+
start_time: @start_time&.iso8601,
|
122
|
+
end_time: Time.now.iso8601,
|
123
|
+
duration: execution_time,
|
124
|
+
final_state: @activity_state,
|
125
|
+
stuck_detected: stuck?,
|
126
|
+
output_count: @output_count
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
# Check if provider supports activity monitoring
|
131
|
+
def supports_activity_monitoring?
|
132
|
+
true # Default to true, override in subclasses if needed
|
133
|
+
end
|
134
|
+
|
135
|
+
# Get stuck timeout for this provider
|
136
|
+
attr_reader :stuck_timeout
|
137
|
+
|
138
|
+
protected
|
139
|
+
|
140
|
+
# Log message to job if in background mode
|
141
|
+
def log_to_job(message, level = "info", metadata = {})
|
142
|
+
return unless @job_context && @job_context[:job_manager]
|
143
|
+
|
144
|
+
metadata = metadata.merge(
|
145
|
+
provider: name,
|
146
|
+
step_name: @step_name,
|
147
|
+
activity_state: @activity_state,
|
148
|
+
execution_time: execution_time,
|
149
|
+
output_count: @output_count
|
150
|
+
)
|
151
|
+
|
152
|
+
@job_context[:job_manager].log_message(
|
153
|
+
@job_context[:job_id],
|
154
|
+
@job_context[:execution_id],
|
155
|
+
message,
|
156
|
+
level,
|
157
|
+
metadata
|
158
|
+
)
|
159
|
+
end
|
13
160
|
end
|
14
161
|
end
|
15
162
|
end
|