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 Gemini < Base
|
8
|
+
def self.available?
|
9
|
+
!!Aidp::Util.which("gemini")
|
10
|
+
end
|
11
|
+
|
12
|
+
def name = "gemini"
|
13
|
+
|
14
|
+
def send(prompt:, session: nil)
|
15
|
+
raise "gemini CLI not available" unless self.class.available?
|
16
|
+
|
17
|
+
require "open3"
|
18
|
+
|
19
|
+
# Use Gemini CLI for non-interactive mode
|
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
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
module Providers
|
7
|
+
class MacOSUI < Base
|
8
|
+
def self.available?
|
9
|
+
RUBY_PLATFORM.include?("darwin")
|
10
|
+
end
|
11
|
+
|
12
|
+
def name = "macos"
|
13
|
+
|
14
|
+
def send(prompt:, session: nil)
|
15
|
+
raise "macOS UI not available on this platform" unless self.class.available?
|
16
|
+
|
17
|
+
# Use macOS UI for interactive mode
|
18
|
+
cmd = ["osascript", "-e", "display dialog \"#{prompt}\" with title \"Aidp\" buttons {\"OK\"} default button \"OK\""]
|
19
|
+
system(*cmd)
|
20
|
+
:ok
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,317 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "agent_supervisor"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
module Providers
|
7
|
+
# Base class for providers that use the agent supervisor
|
8
|
+
class SupervisedBase
|
9
|
+
# Timeout constants are now configurable via environment variables for flexibility
|
10
|
+
DEFAULT_TIMEOUT = Integer(ENV.fetch("AIDP_DEFAULT_TIMEOUT", "300")) # 5 minutes for general operations
|
11
|
+
QUICK_MODE_TIMEOUT = Integer(ENV.fetch("AIDP_QUICK_MODE_TIMEOUT", "120")) # 2 minutes for testing
|
12
|
+
REPOSITORY_ANALYSIS_TIMEOUT = Integer(ENV.fetch("AIDP_REPOSITORY_ANALYSIS_TIMEOUT", "180")) # 3 minutes
|
13
|
+
ARCHITECTURE_ANALYSIS_TIMEOUT = Integer(ENV.fetch("AIDP_ARCHITECTURE_ANALYSIS_TIMEOUT", "600")) # 10 minutes
|
14
|
+
TEST_ANALYSIS_TIMEOUT = Integer(ENV.fetch("AIDP_TEST_ANALYSIS_TIMEOUT", "300")) # 5 minutes
|
15
|
+
FUNCTIONALITY_ANALYSIS_TIMEOUT = Integer(ENV.fetch("AIDP_FUNCTIONALITY_ANALYSIS_TIMEOUT", "600")) # 10 minutes
|
16
|
+
DOCUMENTATION_ANALYSIS_TIMEOUT = Integer(ENV.fetch("AIDP_DOCUMENTATION_ANALYSIS_TIMEOUT", "300")) # 5 minutes
|
17
|
+
STATIC_ANALYSIS_TIMEOUT = Integer(ENV.fetch("AIDP_STATIC_ANALYSIS_TIMEOUT", "450")) # 7.5 minutes
|
18
|
+
REFACTORING_RECOMMENDATIONS_TIMEOUT = Integer(ENV.fetch("AIDP_REFACTORING_RECOMMENDATIONS_TIMEOUT", "600")) # 10 minutes
|
19
|
+
ADAPTIVE_TIMEOUT_BUFFER = Float(ENV.fetch("AIDP_ADAPTIVE_TIMEOUT_BUFFER", "1.2")) # 20% buffer for adaptive timeouts
|
20
|
+
attr_reader :name, :last_execution_result, :metrics
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@last_execution_result = nil
|
24
|
+
@metrics = {
|
25
|
+
total_executions: 0,
|
26
|
+
successful_executions: 0,
|
27
|
+
timeout_count: 0,
|
28
|
+
failure_count: 0,
|
29
|
+
average_duration: 0.0,
|
30
|
+
total_duration: 0.0
|
31
|
+
}
|
32
|
+
@job_context = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
# Abstract method - must be implemented by subclasses
|
36
|
+
def command
|
37
|
+
raise NotImplementedError, "#{self.class} must implement #command"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Abstract method - must be implemented by subclasses
|
41
|
+
def provider_name
|
42
|
+
raise NotImplementedError, "#{self.class} must implement #provider_name"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Set job context for background execution
|
46
|
+
def set_job_context(job_id:, execution_id:, job_manager:)
|
47
|
+
@job_context = {
|
48
|
+
job_id: job_id,
|
49
|
+
execution_id: execution_id,
|
50
|
+
job_manager: job_manager
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# Execute with supervision and recovery
|
55
|
+
def send(prompt:, session: nil)
|
56
|
+
timeout_seconds = calculate_timeout
|
57
|
+
debug = ENV["AIDP_DEBUG"] == "1"
|
58
|
+
|
59
|
+
log_info("Executing with #{provider_name} provider (timeout: #{timeout_seconds}s)")
|
60
|
+
|
61
|
+
# Create supervisor
|
62
|
+
supervisor = AgentSupervisor.new(
|
63
|
+
command,
|
64
|
+
timeout_seconds: timeout_seconds,
|
65
|
+
debug: debug
|
66
|
+
)
|
67
|
+
|
68
|
+
begin
|
69
|
+
# Execute with supervision
|
70
|
+
result = supervisor.execute(prompt)
|
71
|
+
|
72
|
+
# Update metrics
|
73
|
+
update_metrics(supervisor, result)
|
74
|
+
|
75
|
+
# Store result for debugging
|
76
|
+
@last_execution_result = result
|
77
|
+
|
78
|
+
if result[:success]
|
79
|
+
log_info("#{provider_name} completed successfully in #{format_duration(result[:duration])}")
|
80
|
+
result[:output]
|
81
|
+
else
|
82
|
+
handle_execution_failure(result, supervisor)
|
83
|
+
end
|
84
|
+
rescue => e
|
85
|
+
log_error("#{provider_name} execution error: #{e.message}")
|
86
|
+
|
87
|
+
# Try to kill the process if it's still running
|
88
|
+
supervisor.kill! if supervisor.active?
|
89
|
+
|
90
|
+
raise
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get execution statistics
|
95
|
+
def stats
|
96
|
+
@metrics.dup
|
97
|
+
end
|
98
|
+
|
99
|
+
# Reset statistics
|
100
|
+
def reset_stats!
|
101
|
+
@metrics = {
|
102
|
+
total_executions: 0,
|
103
|
+
successful_executions: 0,
|
104
|
+
timeout_count: 0,
|
105
|
+
failure_count: 0,
|
106
|
+
average_duration: 0.0,
|
107
|
+
total_duration: 0.0
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
# Check if provider supports activity monitoring
|
112
|
+
def supports_activity_monitoring?
|
113
|
+
true # Supervised providers always support activity monitoring
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get activity summary for metrics (compatibility with old interface)
|
117
|
+
def activity_summary
|
118
|
+
return {} unless @last_execution_result
|
119
|
+
|
120
|
+
{
|
121
|
+
provider: provider_name,
|
122
|
+
step_name: ENV["AIDP_CURRENT_STEP"],
|
123
|
+
start_time: @last_execution_result[:start_time],
|
124
|
+
end_time: @last_execution_result[:end_time],
|
125
|
+
duration: @last_execution_result[:duration],
|
126
|
+
final_state: @last_execution_result[:state],
|
127
|
+
stuck_detected: false, # Supervisor handles this differently
|
128
|
+
output_count: @last_execution_result[:output_count] || 0
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
# Compatibility methods for old activity monitoring interface
|
133
|
+
def setup_activity_monitoring(step_name, callback = nil, timeout = nil)
|
134
|
+
# No-op for supervised providers - supervisor handles this
|
135
|
+
end
|
136
|
+
|
137
|
+
def record_activity(message = nil)
|
138
|
+
# No-op for supervised providers - supervisor handles this
|
139
|
+
end
|
140
|
+
|
141
|
+
def mark_completed
|
142
|
+
# No-op for supervised providers - supervisor handles this
|
143
|
+
end
|
144
|
+
|
145
|
+
def mark_failed(message = nil)
|
146
|
+
# No-op for supervised providers - supervisor handles this
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def calculate_timeout
|
152
|
+
# Priority order for timeout calculation:
|
153
|
+
# 1. Quick mode (for testing)
|
154
|
+
# 2. Environment variable override
|
155
|
+
# 3. Adaptive timeout based on step type
|
156
|
+
# 4. Default timeout
|
157
|
+
|
158
|
+
if ENV["AIDP_QUICK_MODE"]
|
159
|
+
log_info("Quick mode enabled - #{QUICK_MODE_TIMEOUT / 60} minute timeout")
|
160
|
+
return QUICK_MODE_TIMEOUT
|
161
|
+
end
|
162
|
+
|
163
|
+
provider_timeout_var = "AIDP_#{provider_name.upcase}_TIMEOUT"
|
164
|
+
if ENV[provider_timeout_var]
|
165
|
+
return ENV[provider_timeout_var].to_i
|
166
|
+
end
|
167
|
+
|
168
|
+
# Adaptive timeout based on step type
|
169
|
+
step_timeout = get_adaptive_timeout
|
170
|
+
if step_timeout
|
171
|
+
log_info("Using adaptive timeout: #{step_timeout} seconds")
|
172
|
+
return step_timeout
|
173
|
+
end
|
174
|
+
|
175
|
+
# Default timeout for interactive use
|
176
|
+
log_info("Using default timeout: #{DEFAULT_TIMEOUT / 60} minutes")
|
177
|
+
DEFAULT_TIMEOUT
|
178
|
+
end
|
179
|
+
|
180
|
+
def get_adaptive_timeout
|
181
|
+
# Try to get timeout recommendations from metrics storage
|
182
|
+
begin
|
183
|
+
require_relative "../analyze/metrics_storage"
|
184
|
+
storage = Aidp::Analyze::MetricsStorage.new(Dir.pwd)
|
185
|
+
recommendations = storage.calculate_timeout_recommendations
|
186
|
+
|
187
|
+
# Get current step name from environment or context
|
188
|
+
step_name = ENV["AIDP_CURRENT_STEP"] || "unknown"
|
189
|
+
|
190
|
+
if recommendations[step_name]
|
191
|
+
recommended = recommendations[step_name][:recommended_timeout]
|
192
|
+
# Add buffer for safety
|
193
|
+
return (recommended * ADAPTIVE_TIMEOUT_BUFFER).ceil
|
194
|
+
end
|
195
|
+
rescue => e
|
196
|
+
log_warning("Could not get adaptive timeout: #{e.message}") if ENV["AIDP_DEBUG"]
|
197
|
+
end
|
198
|
+
|
199
|
+
# Fallback timeouts based on step type patterns
|
200
|
+
step_name = ENV["AIDP_CURRENT_STEP"] || ""
|
201
|
+
|
202
|
+
case step_name
|
203
|
+
when /REPOSITORY_ANALYSIS/
|
204
|
+
REPOSITORY_ANALYSIS_TIMEOUT
|
205
|
+
when /ARCHITECTURE_ANALYSIS/
|
206
|
+
ARCHITECTURE_ANALYSIS_TIMEOUT
|
207
|
+
when /TEST_ANALYSIS/
|
208
|
+
TEST_ANALYSIS_TIMEOUT
|
209
|
+
when /FUNCTIONALITY_ANALYSIS/
|
210
|
+
FUNCTIONALITY_ANALYSIS_TIMEOUT
|
211
|
+
when /DOCUMENTATION_ANALYSIS/
|
212
|
+
DOCUMENTATION_ANALYSIS_TIMEOUT
|
213
|
+
when /STATIC_ANALYSIS/
|
214
|
+
STATIC_ANALYSIS_TIMEOUT
|
215
|
+
when /REFACTORING_RECOMMENDATIONS/
|
216
|
+
REFACTORING_RECOMMENDATIONS_TIMEOUT
|
217
|
+
else
|
218
|
+
nil # Use default
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def update_metrics(supervisor, result)
|
223
|
+
@metrics[:total_executions] += 1
|
224
|
+
@metrics[:total_duration] += supervisor.duration
|
225
|
+
@metrics[:average_duration] = @metrics[:total_duration] / @metrics[:total_executions]
|
226
|
+
|
227
|
+
case result[:state]
|
228
|
+
when :completed
|
229
|
+
@metrics[:successful_executions] += 1
|
230
|
+
when :timeout
|
231
|
+
@metrics[:timeout_count] += 1
|
232
|
+
when :failed, :killed
|
233
|
+
@metrics[:failure_count] += 1
|
234
|
+
end
|
235
|
+
|
236
|
+
# Log metrics update if in job context
|
237
|
+
if @job_context
|
238
|
+
@job_context[:job_manager].log_message(
|
239
|
+
@job_context[:job_id],
|
240
|
+
@job_context[:execution_id],
|
241
|
+
"Updated execution metrics",
|
242
|
+
"debug",
|
243
|
+
@metrics
|
244
|
+
)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def handle_execution_failure(result, supervisor)
|
249
|
+
case result[:reason]
|
250
|
+
when "user_aborted"
|
251
|
+
message = "#{provider_name} was aborted by user after #{format_duration(result[:duration])}"
|
252
|
+
log_error(message)
|
253
|
+
raise Interrupt, message
|
254
|
+
when "non_zero_exit"
|
255
|
+
error_msg = result[:error_output].empty? ? "Unknown error" : result[:error_output].strip
|
256
|
+
message = "#{provider_name} failed with exit code #{result[:exit_code]}: #{error_msg}"
|
257
|
+
log_error(message)
|
258
|
+
raise message
|
259
|
+
else
|
260
|
+
message = "#{provider_name} failed: #{result[:reason] || "Unknown error"}"
|
261
|
+
log_error(message)
|
262
|
+
raise message
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def format_duration(seconds)
|
267
|
+
minutes = (seconds / 60).to_i
|
268
|
+
secs = (seconds % 60).to_i
|
269
|
+
|
270
|
+
if minutes > 0
|
271
|
+
"#{minutes}m #{secs}s"
|
272
|
+
else
|
273
|
+
"#{secs}s"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def log_info(message)
|
278
|
+
if @job_context
|
279
|
+
@job_context[:job_manager].log_message(
|
280
|
+
@job_context[:job_id],
|
281
|
+
@job_context[:execution_id],
|
282
|
+
message,
|
283
|
+
"info"
|
284
|
+
)
|
285
|
+
else
|
286
|
+
puts message
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def log_warning(message)
|
291
|
+
if @job_context
|
292
|
+
@job_context[:job_manager].log_message(
|
293
|
+
@job_context[:job_id],
|
294
|
+
@job_context[:execution_id],
|
295
|
+
message,
|
296
|
+
"warning"
|
297
|
+
)
|
298
|
+
else
|
299
|
+
puts "⚠️ #{message}"
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def log_error(message)
|
304
|
+
if @job_context
|
305
|
+
@job_context[:job_manager].log_message(
|
306
|
+
@job_context[:job_id],
|
307
|
+
@job_context[:execution_id],
|
308
|
+
message,
|
309
|
+
"error"
|
310
|
+
)
|
311
|
+
else
|
312
|
+
puts "❌ #{message}"
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "supervised_base"
|
4
|
+
require_relative "../util"
|
5
|
+
|
6
|
+
module Aidp
|
7
|
+
module Providers
|
8
|
+
class SupervisedCursor < SupervisedBase
|
9
|
+
def self.available?
|
10
|
+
!!Aidp::Util.which("cursor-agent")
|
11
|
+
end
|
12
|
+
|
13
|
+
def provider_name
|
14
|
+
"cursor"
|
15
|
+
end
|
16
|
+
|
17
|
+
def command
|
18
|
+
["cursor-agent", "-p"]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/aidp/sync.rb
ADDED
data/lib/aidp/util.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
# Utility functions shared between execute and analyze modes
|
7
|
+
class Util
|
8
|
+
def self.which(cmd)
|
9
|
+
exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
|
10
|
+
ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
|
11
|
+
exts.each do |ext|
|
12
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
13
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.ensure_dirs(output_files, project_dir)
|
20
|
+
output_files.each do |file|
|
21
|
+
dir = File.dirname(File.join(project_dir, file))
|
22
|
+
FileUtils.mkdir_p(dir) unless dir == "."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.safe_file_write(path, content)
|
27
|
+
FileUtils.mkdir_p(File.dirname(path))
|
28
|
+
File.write(path, content)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.project_root?(dir = Dir.pwd)
|
32
|
+
File.exist?(File.join(dir, ".git")) ||
|
33
|
+
File.exist?(File.join(dir, "package.json")) ||
|
34
|
+
File.exist?(File.join(dir, "Gemfile")) ||
|
35
|
+
File.exist?(File.join(dir, "pom.xml")) ||
|
36
|
+
File.exist?(File.join(dir, "build.gradle"))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "digest"
|
5
|
+
|
6
|
+
module Aidp
|
7
|
+
# Workspace management utilities
|
8
|
+
class Workspace
|
9
|
+
def self.current
|
10
|
+
Dir.pwd
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.ensure_project_root
|
14
|
+
unless Aidp::Util.project_root?
|
15
|
+
raise "Not in a project root directory. Please run from a directory with .git, package.json, Gemfile, etc."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|