aidp 0.1.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.
- checksums.yaml +4 -4
- data/README.md +59 -4
- data/bin/aidp +2 -2
- data/lib/aidp/analyze/agent_personas.rb +1 -1
- data/lib/aidp/analyze/data_retention_manager.rb +2 -2
- data/lib/aidp/analyze/database.rb +99 -82
- data/lib/aidp/analyze/error_handler.rb +12 -76
- data/lib/aidp/analyze/focus_guidance.rb +2 -2
- data/lib/aidp/analyze/large_analysis_progress.rb +2 -2
- data/lib/aidp/analyze/metrics_storage.rb +336 -0
- data/lib/aidp/analyze/prioritizer.rb +4 -4
- data/lib/aidp/analyze/repository_chunker.rb +15 -13
- data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
- data/lib/aidp/analyze/runner.rb +107 -191
- data/lib/aidp/analyze/steps.rb +29 -30
- data/lib/aidp/analyze/storage.rb +234 -172
- data/lib/aidp/cli/jobs_command.rb +489 -0
- data/lib/aidp/cli/terminal_io.rb +52 -0
- data/lib/aidp/cli.rb +227 -0
- data/lib/aidp/config.rb +33 -0
- 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/database_migration.rb +158 -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 +47 -0
- data/lib/aidp/jobs/provider_execution_job.rb +96 -0
- data/lib/aidp/project_detector.rb +117 -0
- data/lib/aidp/provider_manager.rb +25 -0
- data/lib/aidp/providers/agent_supervisor.rb +348 -0
- data/lib/aidp/providers/anthropic.rb +187 -0
- data/lib/aidp/providers/base.rb +162 -0
- data/lib/aidp/providers/cursor.rb +304 -0
- data/lib/aidp/providers/gemini.rb +187 -0
- data/lib/aidp/providers/macos_ui.rb +24 -0
- data/lib/aidp/providers/supervised_base.rb +317 -0
- data/lib/aidp/providers/supervised_cursor.rb +22 -0
- data/lib/aidp/sync.rb +13 -0
- data/lib/aidp/util.rb +39 -0
- data/lib/aidp/{shared/version.rb ā version.rb} +1 -3
- data/lib/aidp/workspace.rb +19 -0
- data/lib/aidp.rb +36 -45
- data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
- metadata +89 -45
- data/lib/aidp/shared/cli.rb +0 -117
- data/lib/aidp/shared/config.rb +0 -35
- data/lib/aidp/shared/project_detector.rb +0 -119
- data/lib/aidp/shared/providers/anthropic.rb +0 -26
- data/lib/aidp/shared/providers/base.rb +0 -17
- data/lib/aidp/shared/providers/cursor.rb +0 -102
- data/lib/aidp/shared/providers/gemini.rb +0 -26
- data/lib/aidp/shared/providers/macos_ui.rb +0 -26
- data/lib/aidp/shared/sync.rb +0 -15
- data/lib/aidp/shared/util.rb +0 -41
- data/lib/aidp/shared/workspace.rb +0 -21
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
module Providers
|
7
|
+
class Anthropic < Base
|
8
|
+
def self.available?
|
9
|
+
!!Aidp::Util.which("claude")
|
10
|
+
end
|
11
|
+
|
12
|
+
def name = "anthropic"
|
13
|
+
|
14
|
+
def send(prompt:, session: nil)
|
15
|
+
raise "claude CLI not available" unless self.class.available?
|
16
|
+
|
17
|
+
require "open3"
|
18
|
+
|
19
|
+
# Use Claude CLI for non-interactive mode
|
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
|
+
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 "ā
Claude analysis completed"
|
76
|
+
mark_completed
|
77
|
+
return output.empty? ? :ok : output
|
78
|
+
else
|
79
|
+
error_output = stderr.read
|
80
|
+
mark_failed("claude failed with exit code #{result.exitstatus}: #{error_output}")
|
81
|
+
raise "claude 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("claude timed out after #{timeout_seconds} seconds")
|
96
|
+
raise Timeout::Error, "claude 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("claude 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_ANTHROPIC_TIMEOUT"]
|
129
|
+
return ENV["AIDP_ANTHROPIC_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
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aidp
|
4
|
+
module Providers
|
5
|
+
class Base
|
6
|
+
# Activity indicator states
|
7
|
+
ACTIVITY_STATES = {
|
8
|
+
idle: "ā³",
|
9
|
+
working: "š",
|
10
|
+
stuck: "ā ļø",
|
11
|
+
completed: "ā
",
|
12
|
+
failed: "ā"
|
13
|
+
}.freeze
|
14
|
+
|
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
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,304 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
require "timeout"
|
5
|
+
require_relative "base"
|
6
|
+
require_relative "../util"
|
7
|
+
|
8
|
+
module Aidp
|
9
|
+
module Providers
|
10
|
+
class Cursor < Base
|
11
|
+
def self.available?
|
12
|
+
!!Aidp::Util.which("cursor-agent")
|
13
|
+
end
|
14
|
+
|
15
|
+
def name = "cursor"
|
16
|
+
|
17
|
+
def send(prompt:, session: nil)
|
18
|
+
raise "cursor-agent not available" unless self.class.available?
|
19
|
+
|
20
|
+
# Always use non-interactive mode with -p flag
|
21
|
+
cmd = ["cursor-agent", "-p"]
|
22
|
+
puts "š Sending prompt to cursor-agent"
|
23
|
+
|
24
|
+
# Enable debug output if requested
|
25
|
+
if ENV["AIDP_DEBUG"]
|
26
|
+
puts "š Debug mode enabled - showing cursor-agent output"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Setup logging if log file is specified
|
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
|
48
|
+
end
|
49
|
+
|
50
|
+
Open3.popen3(*cmd) do |stdin, stdout, stderr, wait|
|
51
|
+
# Send the prompt to stdin
|
52
|
+
stdin.puts prompt
|
53
|
+
stdin.close
|
54
|
+
|
55
|
+
# Read stdout and stderr synchronously for better reliability
|
56
|
+
output = ""
|
57
|
+
error_output = ""
|
58
|
+
|
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
|
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]}...")
|
72
|
+
end
|
73
|
+
rescue IOError => e
|
74
|
+
puts "š¤ stdout stream closed: #{e.message}" if ENV["AIDP_DEBUG"]
|
75
|
+
end
|
76
|
+
|
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
|
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]}...")
|
90
|
+
end
|
91
|
+
rescue IOError => e
|
92
|
+
puts "ā stderr stream closed: #{e.message}" if ENV["AIDP_DEBUG"]
|
93
|
+
end
|
94
|
+
|
95
|
+
# Start activity monitoring thread
|
96
|
+
activity_thread = Thread.new do
|
97
|
+
loop do
|
98
|
+
sleep 10 # Check every 10 seconds
|
99
|
+
|
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
|
120
|
+
end
|
121
|
+
|
122
|
+
# Stop checking if the process is done
|
123
|
+
break if wait.value
|
124
|
+
rescue
|
125
|
+
break
|
126
|
+
end
|
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
|
141
|
+
end
|
142
|
+
|
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
|
176
|
+
end
|
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}"
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|