aidp 0.22.0 → 0.24.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 +145 -31
- data/lib/aidp/cli.rb +19 -2
- data/lib/aidp/execute/work_loop_runner.rb +252 -45
- data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
- data/lib/aidp/harness/condition_detector.rb +42 -8
- data/lib/aidp/harness/config_manager.rb +7 -0
- data/lib/aidp/harness/config_schema.rb +25 -0
- data/lib/aidp/harness/configuration.rb +69 -6
- data/lib/aidp/harness/error_handler.rb +117 -44
- data/lib/aidp/harness/provider_manager.rb +64 -0
- data/lib/aidp/harness/provider_metrics.rb +138 -0
- data/lib/aidp/harness/runner.rb +110 -35
- data/lib/aidp/harness/simple_user_interface.rb +4 -0
- data/lib/aidp/harness/state/ui_state.rb +0 -10
- data/lib/aidp/harness/state_manager.rb +1 -15
- data/lib/aidp/harness/test_runner.rb +39 -2
- data/lib/aidp/logger.rb +34 -4
- data/lib/aidp/providers/adapter.rb +241 -0
- data/lib/aidp/providers/anthropic.rb +75 -7
- data/lib/aidp/providers/base.rb +29 -1
- data/lib/aidp/providers/capability_registry.rb +205 -0
- data/lib/aidp/providers/codex.rb +14 -0
- data/lib/aidp/providers/error_taxonomy.rb +195 -0
- data/lib/aidp/providers/gemini.rb +3 -2
- data/lib/aidp/setup/devcontainer/backup_manager.rb +11 -4
- data/lib/aidp/setup/provider_registry.rb +107 -0
- data/lib/aidp/setup/wizard.rb +189 -31
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +357 -27
- data/lib/aidp/watch/plan_generator.rb +16 -1
- data/lib/aidp/watch/plan_processor.rb +54 -3
- data/lib/aidp/watch/repository_client.rb +78 -4
- data/lib/aidp/watch/repository_safety_checker.rb +12 -3
- data/lib/aidp/watch/runner.rb +52 -10
- data/lib/aidp/workflows/guided_agent.rb +53 -0
- data/lib/aidp/worktree.rb +67 -10
- data/templates/work_loop/decide_whats_next.md +21 -0
- data/templates/work_loop/diagnose_failures.md +21 -0
- metadata +10 -3
- /data/{bin → exe}/aidp +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "tty-prompt"
|
|
4
4
|
require_relative "provider_factory"
|
|
5
|
+
require_relative "provider_metrics"
|
|
5
6
|
require_relative "../rescue_logging"
|
|
6
7
|
require_relative "../concurrency"
|
|
7
8
|
|
|
@@ -40,6 +41,20 @@ module Aidp
|
|
|
40
41
|
@unavailable_cache = {}
|
|
41
42
|
@binary_check_cache = {}
|
|
42
43
|
@binary_check_ttl = 300 # seconds
|
|
44
|
+
|
|
45
|
+
# Initialize persistence
|
|
46
|
+
project_dir = if configuration.respond_to?(:project_dir)
|
|
47
|
+
configuration.project_dir
|
|
48
|
+
elsif configuration.respond_to?(:root_dir)
|
|
49
|
+
configuration.root_dir
|
|
50
|
+
else
|
|
51
|
+
Dir.pwd
|
|
52
|
+
end
|
|
53
|
+
@metrics_persistence = ProviderMetrics.new(project_dir)
|
|
54
|
+
|
|
55
|
+
# Load persisted metrics
|
|
56
|
+
load_persisted_metrics
|
|
57
|
+
|
|
43
58
|
initialize_fallback_chains
|
|
44
59
|
initialize_provider_health
|
|
45
60
|
initialize_model_configs
|
|
@@ -932,6 +947,9 @@ module Aidp
|
|
|
932
947
|
# Update provider health
|
|
933
948
|
update_provider_health(provider_name, "rate_limited")
|
|
934
949
|
|
|
950
|
+
# Persist rate limit info to disk
|
|
951
|
+
save_persisted_rate_limits
|
|
952
|
+
|
|
935
953
|
# Switch to next provider if current one is rate limited
|
|
936
954
|
if provider_name == current_provider
|
|
937
955
|
switch_provider("rate_limit", {provider: provider_name})
|
|
@@ -996,6 +1014,9 @@ module Aidp
|
|
|
996
1014
|
metrics[:last_error_time] = Time.now
|
|
997
1015
|
update_provider_health(provider_name, "error", {error: error})
|
|
998
1016
|
end
|
|
1017
|
+
|
|
1018
|
+
# Persist metrics to disk
|
|
1019
|
+
save_persisted_metrics
|
|
999
1020
|
end
|
|
1000
1021
|
|
|
1001
1022
|
# Record model metrics
|
|
@@ -1611,6 +1632,49 @@ module Aidp
|
|
|
1611
1632
|
# Most models reset rate limits every hour
|
|
1612
1633
|
Time.now + (60 * 60)
|
|
1613
1634
|
end
|
|
1635
|
+
|
|
1636
|
+
# Load persisted metrics from disk
|
|
1637
|
+
def load_persisted_metrics
|
|
1638
|
+
return unless @metrics_persistence
|
|
1639
|
+
|
|
1640
|
+
# Load provider metrics
|
|
1641
|
+
persisted_metrics = @metrics_persistence.load_metrics
|
|
1642
|
+
@provider_metrics.merge!(persisted_metrics) if persisted_metrics.is_a?(Hash)
|
|
1643
|
+
|
|
1644
|
+
# Load rate limit info
|
|
1645
|
+
persisted_rate_limits = @metrics_persistence.load_rate_limits
|
|
1646
|
+
@rate_limit_info.merge!(persisted_rate_limits) if persisted_rate_limits.is_a?(Hash)
|
|
1647
|
+
|
|
1648
|
+
# Clean up expired rate limits
|
|
1649
|
+
cleanup_expired_rate_limits
|
|
1650
|
+
rescue => e
|
|
1651
|
+
log_rescue(e, component: "provider_manager", action: "load_persisted_metrics", fallback: nil)
|
|
1652
|
+
end
|
|
1653
|
+
|
|
1654
|
+
# Save persisted metrics to disk
|
|
1655
|
+
def save_persisted_metrics
|
|
1656
|
+
return unless @metrics_persistence
|
|
1657
|
+
@metrics_persistence.save_metrics(@provider_metrics)
|
|
1658
|
+
rescue => e
|
|
1659
|
+
log_rescue(e, component: "provider_manager", action: "save_persisted_metrics", fallback: nil)
|
|
1660
|
+
end
|
|
1661
|
+
|
|
1662
|
+
# Save persisted rate limits to disk
|
|
1663
|
+
def save_persisted_rate_limits
|
|
1664
|
+
return unless @metrics_persistence
|
|
1665
|
+
@metrics_persistence.save_rate_limits(@rate_limit_info)
|
|
1666
|
+
rescue => e
|
|
1667
|
+
log_rescue(e, component: "provider_manager", action: "save_persisted_rate_limits", fallback: nil)
|
|
1668
|
+
end
|
|
1669
|
+
|
|
1670
|
+
# Clean up expired rate limits from memory
|
|
1671
|
+
def cleanup_expired_rate_limits
|
|
1672
|
+
now = Time.now
|
|
1673
|
+
@rate_limit_info.delete_if do |_provider, info|
|
|
1674
|
+
reset_time = info[:reset_time]
|
|
1675
|
+
reset_time && now >= reset_time
|
|
1676
|
+
end
|
|
1677
|
+
end
|
|
1614
1678
|
end
|
|
1615
1679
|
end
|
|
1616
1680
|
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "../rescue_logging"
|
|
6
|
+
|
|
7
|
+
module Aidp
|
|
8
|
+
module Harness
|
|
9
|
+
# Persists provider metrics and rate limit information to disk
|
|
10
|
+
# Enables the provider dashboard to display real-time state
|
|
11
|
+
class ProviderMetrics
|
|
12
|
+
include Aidp::RescueLogging
|
|
13
|
+
|
|
14
|
+
attr_reader :project_dir, :metrics_file, :rate_limit_file
|
|
15
|
+
|
|
16
|
+
def initialize(project_dir)
|
|
17
|
+
@project_dir = project_dir
|
|
18
|
+
@metrics_file = File.join(project_dir, ".aidp", "provider_metrics.yml")
|
|
19
|
+
@rate_limit_file = File.join(project_dir, ".aidp", "provider_rate_limits.yml")
|
|
20
|
+
ensure_directory
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Save provider metrics to disk
|
|
24
|
+
def save_metrics(metrics_hash)
|
|
25
|
+
return if metrics_hash.nil? || metrics_hash.empty?
|
|
26
|
+
|
|
27
|
+
# Convert Time objects to ISO8601 strings for YAML serialization
|
|
28
|
+
serializable_metrics = serialize_metrics(metrics_hash)
|
|
29
|
+
|
|
30
|
+
File.write(@metrics_file, YAML.dump(serializable_metrics))
|
|
31
|
+
rescue => e
|
|
32
|
+
log_rescue(e, component: "provider_metrics", action: "save_metrics", fallback: nil)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Load provider metrics from disk
|
|
36
|
+
def load_metrics
|
|
37
|
+
return {} unless File.exist?(@metrics_file)
|
|
38
|
+
|
|
39
|
+
data = YAML.safe_load_file(@metrics_file, permitted_classes: [Time, Date, Symbol], aliases: true)
|
|
40
|
+
return {} unless data.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
# Convert ISO8601 strings back to Time objects
|
|
43
|
+
deserialize_metrics(data)
|
|
44
|
+
rescue => e
|
|
45
|
+
log_rescue(e, component: "provider_metrics", action: "load_metrics", fallback: {})
|
|
46
|
+
{}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Save rate limit information to disk
|
|
50
|
+
def save_rate_limits(rate_limit_hash)
|
|
51
|
+
return if rate_limit_hash.nil? || rate_limit_hash.empty?
|
|
52
|
+
|
|
53
|
+
# Convert Time objects to ISO8601 strings for YAML serialization
|
|
54
|
+
serializable_rate_limits = serialize_rate_limits(rate_limit_hash)
|
|
55
|
+
|
|
56
|
+
File.write(@rate_limit_file, YAML.dump(serializable_rate_limits))
|
|
57
|
+
rescue => e
|
|
58
|
+
log_rescue(e, component: "provider_metrics", action: "save_rate_limits", fallback: nil)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Load rate limit information from disk
|
|
62
|
+
def load_rate_limits
|
|
63
|
+
return {} unless File.exist?(@rate_limit_file)
|
|
64
|
+
|
|
65
|
+
data = YAML.safe_load_file(@rate_limit_file, permitted_classes: [Time, Date, Symbol], aliases: true)
|
|
66
|
+
return {} unless data.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
# Convert ISO8601 strings back to Time objects
|
|
69
|
+
deserialize_rate_limits(data)
|
|
70
|
+
rescue => e
|
|
71
|
+
log_rescue(e, component: "provider_metrics", action: "load_rate_limits", fallback: {})
|
|
72
|
+
{}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Clear all persisted metrics
|
|
76
|
+
def clear
|
|
77
|
+
File.delete(@metrics_file) if File.exist?(@metrics_file)
|
|
78
|
+
File.delete(@rate_limit_file) if File.exist?(@rate_limit_file)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def ensure_directory
|
|
84
|
+
aidp_dir = File.join(@project_dir, ".aidp")
|
|
85
|
+
FileUtils.mkdir_p(aidp_dir) unless File.directory?(aidp_dir)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def serialize_metrics(metrics_hash)
|
|
89
|
+
metrics_hash.transform_values do |provider_metrics|
|
|
90
|
+
next provider_metrics unless provider_metrics.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
provider_metrics.transform_values do |value|
|
|
93
|
+
value.is_a?(Time) ? value.iso8601 : value
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def deserialize_metrics(metrics_hash)
|
|
99
|
+
metrics_hash.transform_values do |provider_metrics|
|
|
100
|
+
next provider_metrics unless provider_metrics.is_a?(Hash)
|
|
101
|
+
|
|
102
|
+
provider_metrics.transform_keys(&:to_sym).transform_values do |value|
|
|
103
|
+
parse_time_if_string(value)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def serialize_rate_limits(rate_limit_hash)
|
|
109
|
+
rate_limit_hash.transform_values do |limit_info|
|
|
110
|
+
next limit_info unless limit_info.is_a?(Hash)
|
|
111
|
+
|
|
112
|
+
limit_info.transform_values do |value|
|
|
113
|
+
value.is_a?(Time) ? value.iso8601 : value
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def deserialize_rate_limits(rate_limit_hash)
|
|
119
|
+
rate_limit_hash.transform_values do |limit_info|
|
|
120
|
+
next limit_info unless limit_info.is_a?(Hash)
|
|
121
|
+
|
|
122
|
+
limit_info.transform_keys(&:to_sym).transform_values do |value|
|
|
123
|
+
parse_time_if_string(value)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def parse_time_if_string(value)
|
|
129
|
+
return value unless value.is_a?(String)
|
|
130
|
+
|
|
131
|
+
# Try to parse ISO8601 timestamp
|
|
132
|
+
Time.parse(value)
|
|
133
|
+
rescue ArgumentError
|
|
134
|
+
value
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/lib/aidp/harness/runner.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "timeout"
|
|
4
4
|
require "json"
|
|
5
|
-
require_relative "
|
|
5
|
+
require_relative "configuration"
|
|
6
6
|
require_relative "state_manager"
|
|
7
7
|
require_relative "condition_detector"
|
|
8
8
|
require_relative "provider_manager"
|
|
@@ -27,11 +27,12 @@ module Aidp
|
|
|
27
27
|
waiting_for_rate_limit: "waiting_for_rate_limit",
|
|
28
28
|
stopped: "stopped",
|
|
29
29
|
completed: "completed",
|
|
30
|
-
error: "error"
|
|
30
|
+
error: "error",
|
|
31
|
+
needs_clarification: "needs_clarification"
|
|
31
32
|
}.freeze
|
|
32
33
|
|
|
33
34
|
# Public accessors for testing and integration
|
|
34
|
-
attr_reader :current_provider, :current_step, :user_input, :execution_log, :provider_manager
|
|
35
|
+
attr_reader :current_provider, :current_step, :user_input, :execution_log, :provider_manager, :clarification_questions
|
|
35
36
|
|
|
36
37
|
def initialize(project_dir, mode = :analyze, options = {})
|
|
37
38
|
@project_dir = project_dir
|
|
@@ -43,27 +44,31 @@ module Aidp
|
|
|
43
44
|
@current_provider = nil
|
|
44
45
|
@user_input = options[:user_input] || {} # Include user input from workflow selection
|
|
45
46
|
@execution_log = []
|
|
47
|
+
@last_error = nil
|
|
46
48
|
@prompt = options[:prompt] || TTY::Prompt.new
|
|
47
49
|
|
|
48
50
|
# Store workflow configuration
|
|
49
51
|
@selected_steps = options[:selected_steps]
|
|
50
52
|
@workflow_type = options[:workflow_type]
|
|
53
|
+
@non_interactive = options[:non_interactive] || (@workflow_type == :watch_mode)
|
|
51
54
|
|
|
52
55
|
# Initialize components
|
|
53
|
-
@
|
|
56
|
+
@configuration = Configuration.new(project_dir)
|
|
54
57
|
@state_manager = StateManager.new(project_dir, @mode)
|
|
55
|
-
@provider_manager = ProviderManager.new(@
|
|
58
|
+
@provider_manager = ProviderManager.new(@configuration, prompt: @prompt)
|
|
56
59
|
|
|
57
60
|
# Use ZFC-enabled condition detector
|
|
58
61
|
# ZfcConditionDetector will create its own ProviderFactory if needed
|
|
59
62
|
# Falls back to legacy pattern matching when ZFC is disabled
|
|
60
63
|
require_relative "zfc_condition_detector"
|
|
61
|
-
@condition_detector = ZfcConditionDetector.new(@
|
|
64
|
+
@condition_detector = ZfcConditionDetector.new(@configuration)
|
|
62
65
|
|
|
63
66
|
@user_interface = SimpleUserInterface.new
|
|
64
|
-
@error_handler = ErrorHandler.new(@provider_manager, @
|
|
67
|
+
@error_handler = ErrorHandler.new(@provider_manager, @configuration)
|
|
65
68
|
@status_display = StatusDisplay.new
|
|
66
69
|
@completion_checker = CompletionChecker.new(@project_dir, @workflow_type)
|
|
70
|
+
@failure_reason = nil
|
|
71
|
+
@failure_metadata = nil
|
|
67
72
|
end
|
|
68
73
|
|
|
69
74
|
# Main execution method - runs the harness loop
|
|
@@ -113,26 +118,56 @@ module Aidp
|
|
|
113
118
|
display_message(completion_status[:summary], type: :info)
|
|
114
119
|
|
|
115
120
|
# Ask user if they want to continue anyway
|
|
116
|
-
if
|
|
117
|
-
@
|
|
118
|
-
|
|
121
|
+
if confirmation_prompt_allowed?
|
|
122
|
+
if @user_interface.get_confirmation("Continue anyway? This may indicate issues that should be addressed.", default: false)
|
|
123
|
+
@state = STATES[:completed]
|
|
124
|
+
log_execution("Harness completed with user override")
|
|
125
|
+
else
|
|
126
|
+
mark_completion_failure(completion_status)
|
|
127
|
+
@state = STATES[:error]
|
|
128
|
+
log_execution("Harness stopped due to unmet completion criteria")
|
|
129
|
+
end
|
|
119
130
|
else
|
|
131
|
+
display_message("⚠️ Non-interactive mode: cannot override failed completion criteria. Stopping run.", type: :warning)
|
|
132
|
+
mark_completion_failure(completion_status)
|
|
120
133
|
@state = STATES[:error]
|
|
121
|
-
log_execution("Harness stopped due to unmet completion criteria")
|
|
134
|
+
log_execution("Harness stopped due to unmet completion criteria in non-interactive mode")
|
|
122
135
|
end
|
|
123
136
|
end
|
|
124
137
|
end
|
|
125
138
|
rescue => e
|
|
126
139
|
@state = STATES[:error]
|
|
127
|
-
|
|
140
|
+
@last_error = e
|
|
141
|
+
log_execution("Harness error: #{e.message}", {error: e.class.name, backtrace: e.backtrace&.first(5)})
|
|
128
142
|
handle_error(e)
|
|
129
143
|
ensure
|
|
130
|
-
# Save state before exiting
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
# Save state before exiting - protect against exceptions during cleanup
|
|
145
|
+
begin
|
|
146
|
+
save_state
|
|
147
|
+
rescue => e
|
|
148
|
+
# Don't let state save failures kill the whole run or prevent cleanup
|
|
149
|
+
Aidp.logger.error("harness", "Failed to save state during cleanup: #{e.message}", error: e.class.name)
|
|
150
|
+
@last_error ||= e # Only set if no previous error
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
begin
|
|
154
|
+
cleanup
|
|
155
|
+
rescue => e
|
|
156
|
+
# Don't let cleanup failures propagate
|
|
157
|
+
Aidp.logger.error("harness", "Failed during cleanup: #{e.message}", error: e.class.name)
|
|
158
|
+
end
|
|
133
159
|
end
|
|
134
160
|
|
|
135
|
-
{status: @state, message: get_completion_message}
|
|
161
|
+
result = {status: @state, message: get_completion_message}
|
|
162
|
+
result[:reason] = @failure_reason if @failure_reason
|
|
163
|
+
result[:failure_metadata] = @failure_metadata if @failure_metadata
|
|
164
|
+
result[:clarification_questions] = @clarification_questions if @clarification_questions
|
|
165
|
+
if @last_error
|
|
166
|
+
result[:error] = @last_error.message
|
|
167
|
+
result[:error_class] = @last_error.class.name
|
|
168
|
+
result[:backtrace] = @last_error.backtrace&.first(10)
|
|
169
|
+
end
|
|
170
|
+
result
|
|
136
171
|
end
|
|
137
172
|
|
|
138
173
|
# Pause the harness execution
|
|
@@ -180,9 +215,9 @@ module Aidp
|
|
|
180
215
|
{
|
|
181
216
|
harness: status,
|
|
182
217
|
configuration: {
|
|
183
|
-
default_provider: @
|
|
184
|
-
fallback_providers: @
|
|
185
|
-
max_retries: @
|
|
218
|
+
default_provider: @configuration.default_provider,
|
|
219
|
+
fallback_providers: @configuration.fallback_providers,
|
|
220
|
+
max_retries: @configuration.harness_config[:max_retries]
|
|
186
221
|
},
|
|
187
222
|
provider_manager: @provider_manager.status,
|
|
188
223
|
error_stats: @error_handler.error_stats
|
|
@@ -248,12 +283,23 @@ module Aidp
|
|
|
248
283
|
end
|
|
249
284
|
|
|
250
285
|
def handle_user_feedback_request(result)
|
|
251
|
-
@state = STATES[:waiting_for_user]
|
|
252
|
-
log_execution("Waiting for user feedback")
|
|
253
|
-
|
|
254
286
|
# Extract questions from result
|
|
255
287
|
questions = @condition_detector.extract_questions(result)
|
|
256
288
|
|
|
289
|
+
# Check if we're in watch mode (non-interactive)
|
|
290
|
+
if @options[:workflow_type] == :watch_mode
|
|
291
|
+
# Store questions for later retrieval and set state to needs_clarification
|
|
292
|
+
@clarification_questions = questions
|
|
293
|
+
@state = STATES[:needs_clarification]
|
|
294
|
+
log_execution("Clarification needed in watch mode", {question_count: questions.size})
|
|
295
|
+
# Don't continue - exit the loop so we can return this status
|
|
296
|
+
return
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Interactive mode: collect feedback from user
|
|
300
|
+
@state = STATES[:waiting_for_user]
|
|
301
|
+
log_execution("Waiting for user feedback")
|
|
302
|
+
|
|
257
303
|
# Collect user input
|
|
258
304
|
user_responses = @user_interface.collect_feedback(questions)
|
|
259
305
|
|
|
@@ -267,15 +313,30 @@ module Aidp
|
|
|
267
313
|
log_execution("User feedback collected", {responses: user_responses.keys})
|
|
268
314
|
end
|
|
269
315
|
|
|
270
|
-
def handle_rate_limit(
|
|
316
|
+
def handle_rate_limit(result)
|
|
271
317
|
@state = STATES[:waiting_for_rate_limit]
|
|
272
318
|
log_execution("Rate limit detected, switching provider")
|
|
273
319
|
|
|
320
|
+
rate_limit_info = nil
|
|
321
|
+
if @condition_detector.respond_to?(:extract_rate_limit_info)
|
|
322
|
+
rate_limit_info = @condition_detector.extract_rate_limit_info(result, @current_provider)
|
|
323
|
+
end
|
|
324
|
+
reset_time = rate_limit_info && rate_limit_info[:reset_time]
|
|
325
|
+
|
|
274
326
|
# Mark current provider as rate limited
|
|
275
|
-
@provider_manager.mark_rate_limited(@current_provider)
|
|
327
|
+
@provider_manager.mark_rate_limited(@current_provider, reset_time)
|
|
328
|
+
|
|
329
|
+
# Provider manager might already have switched upstream (e.g., during CLI execution)
|
|
330
|
+
manager_current = @provider_manager.current_provider
|
|
331
|
+
if manager_current && manager_current != @current_provider
|
|
332
|
+
@current_provider = manager_current
|
|
333
|
+
@state = STATES[:running]
|
|
334
|
+
log_execution("Provider already switched upstream", new_provider: manager_current)
|
|
335
|
+
return
|
|
336
|
+
end
|
|
276
337
|
|
|
277
|
-
# Switch to next provider
|
|
278
|
-
next_provider = @provider_manager.switch_provider
|
|
338
|
+
# Switch to next provider explicitly when still on the rate-limited provider
|
|
339
|
+
next_provider = @provider_manager.switch_provider("rate_limit", previous_provider: @current_provider)
|
|
279
340
|
@current_provider = next_provider
|
|
280
341
|
|
|
281
342
|
if next_provider
|
|
@@ -299,6 +360,10 @@ module Aidp
|
|
|
299
360
|
end
|
|
300
361
|
end
|
|
301
362
|
|
|
363
|
+
def confirmation_prompt_allowed?
|
|
364
|
+
!@non_interactive
|
|
365
|
+
end
|
|
366
|
+
|
|
302
367
|
def sleep_until_reset(reset_time)
|
|
303
368
|
while Time.now < reset_time && @state == STATES[:waiting_for_rate_limit]
|
|
304
369
|
remaining = reset_time - Time.now
|
|
@@ -360,20 +425,14 @@ module Aidp
|
|
|
360
425
|
end
|
|
361
426
|
|
|
362
427
|
def save_state
|
|
363
|
-
# Save harness-specific state
|
|
428
|
+
# Save harness-specific state (execution_log removed to prevent unbounded growth)
|
|
364
429
|
@state_manager.save_state({
|
|
365
430
|
state: @state,
|
|
366
431
|
current_step: @current_step,
|
|
367
432
|
current_provider: @current_provider,
|
|
368
433
|
user_input: @user_input,
|
|
369
|
-
execution_log: @execution_log,
|
|
370
434
|
last_saved: Time.now
|
|
371
435
|
})
|
|
372
|
-
|
|
373
|
-
# Also save execution log entries to state manager
|
|
374
|
-
@execution_log.each do |entry|
|
|
375
|
-
@state_manager.add_execution_log(entry)
|
|
376
|
-
end
|
|
377
436
|
end
|
|
378
437
|
|
|
379
438
|
def handle_error(error)
|
|
@@ -386,6 +445,7 @@ module Aidp
|
|
|
386
445
|
end
|
|
387
446
|
|
|
388
447
|
def log_execution(message, data = {})
|
|
448
|
+
# Keep in-memory log for runtime diagnostics (not persisted)
|
|
389
449
|
log_entry = {
|
|
390
450
|
timestamp: Time.now,
|
|
391
451
|
message: message,
|
|
@@ -394,7 +454,13 @@ module Aidp
|
|
|
394
454
|
}
|
|
395
455
|
@execution_log << log_entry
|
|
396
456
|
|
|
397
|
-
#
|
|
457
|
+
# Log to persistent logger instead of state file
|
|
458
|
+
Aidp.logger.info("harness_execution", message,
|
|
459
|
+
state: @state,
|
|
460
|
+
step: @current_step,
|
|
461
|
+
**data.slice(:error, :error_class, :criteria, :all_complete, :summary).compact)
|
|
462
|
+
|
|
463
|
+
# Also log to standard output in debug mode
|
|
398
464
|
puts "[#{Time.now.strftime("%H:%M:%S")}] #{message}" if ENV["AIDP_DEBUG"] == "1"
|
|
399
465
|
end
|
|
400
466
|
|
|
@@ -405,12 +471,21 @@ module Aidp
|
|
|
405
471
|
when STATES[:stopped]
|
|
406
472
|
"Harness stopped by user."
|
|
407
473
|
when STATES[:error]
|
|
408
|
-
|
|
474
|
+
if @last_error
|
|
475
|
+
"Harness encountered an error and stopped: #{@last_error.class.name}: #{@last_error.message}"
|
|
476
|
+
else
|
|
477
|
+
"Harness encountered an error and stopped."
|
|
478
|
+
end
|
|
409
479
|
else
|
|
410
480
|
"Harness finished in state: #{@state}"
|
|
411
481
|
end
|
|
412
482
|
end
|
|
413
483
|
|
|
484
|
+
def mark_completion_failure(completion_status)
|
|
485
|
+
@failure_reason = :completion_criteria
|
|
486
|
+
@failure_metadata = completion_status
|
|
487
|
+
end
|
|
488
|
+
|
|
414
489
|
private
|
|
415
490
|
end
|
|
416
491
|
end
|
|
@@ -19,16 +19,6 @@ module Aidp
|
|
|
19
19
|
update_state(user_input: current_input)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def execution_log
|
|
23
|
-
state[:execution_log] || []
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def add_execution_log(entry)
|
|
27
|
-
current_log = execution_log
|
|
28
|
-
current_log << entry
|
|
29
|
-
update_state(execution_log: current_log)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
22
|
def current_step
|
|
33
23
|
state[:current_step]
|
|
34
24
|
end
|
|
@@ -129,20 +129,6 @@ module Aidp
|
|
|
129
129
|
update_state(user_input: current_input, last_updated: Time.now)
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
-
# Get execution log
|
|
133
|
-
def execution_log
|
|
134
|
-
state = load_state
|
|
135
|
-
return [] unless state
|
|
136
|
-
state[:execution_log] || []
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Add to execution log
|
|
140
|
-
def add_execution_log(entry)
|
|
141
|
-
current_log = execution_log
|
|
142
|
-
current_log << entry
|
|
143
|
-
update_state(execution_log: current_log, last_updated: Time.now)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
132
|
# Get provider state
|
|
147
133
|
def provider_state
|
|
148
134
|
state = load_state
|
|
@@ -615,7 +601,7 @@ module Aidp
|
|
|
615
601
|
yield
|
|
616
602
|
end
|
|
617
603
|
end
|
|
618
|
-
rescue Aidp::Concurrency::
|
|
604
|
+
rescue Aidp::Concurrency::MaxAttemptsError
|
|
619
605
|
raise "Could not acquire state lock within timeout"
|
|
620
606
|
ensure
|
|
621
607
|
# Clean up lock file
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
|
+
require_relative "../tooling_detector"
|
|
4
5
|
|
|
5
6
|
module Aidp
|
|
6
7
|
module Harness
|
|
@@ -15,7 +16,7 @@ module Aidp
|
|
|
15
16
|
# Run all configured tests
|
|
16
17
|
# Returns: { success: boolean, output: string, failures: array }
|
|
17
18
|
def run_tests
|
|
18
|
-
test_commands =
|
|
19
|
+
test_commands = resolved_test_commands
|
|
19
20
|
return {success: true, output: "", failures: []} if test_commands.empty?
|
|
20
21
|
|
|
21
22
|
results = test_commands.map { |cmd| execute_command(cmd, "test") }
|
|
@@ -25,7 +26,7 @@ module Aidp
|
|
|
25
26
|
# Run all configured linters
|
|
26
27
|
# Returns: { success: boolean, output: string, failures: array }
|
|
27
28
|
def run_linters
|
|
28
|
-
lint_commands =
|
|
29
|
+
lint_commands = resolved_lint_commands
|
|
29
30
|
return {success: true, output: "", failures: []} if lint_commands.empty?
|
|
30
31
|
|
|
31
32
|
results = lint_commands.map { |cmd| execute_command(cmd, "linter") }
|
|
@@ -78,6 +79,42 @@ module Aidp
|
|
|
78
79
|
|
|
79
80
|
output.join("\n")
|
|
80
81
|
end
|
|
82
|
+
|
|
83
|
+
def resolved_test_commands
|
|
84
|
+
explicit = Array(@config.test_commands).compact.map(&:strip).reject(&:empty?)
|
|
85
|
+
return explicit unless explicit.empty?
|
|
86
|
+
|
|
87
|
+
detected = detected_tooling.test_commands
|
|
88
|
+
log_fallback(:tests, detected) unless detected.empty?
|
|
89
|
+
detected
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def resolved_lint_commands
|
|
93
|
+
explicit = Array(@config.lint_commands).compact.map(&:strip).reject(&:empty?)
|
|
94
|
+
return explicit unless explicit.empty?
|
|
95
|
+
|
|
96
|
+
detected = detected_tooling.lint_commands
|
|
97
|
+
log_fallback(:linters, detected) unless detected.empty?
|
|
98
|
+
detected
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def detected_tooling
|
|
102
|
+
@detected_tooling ||= Aidp::ToolingDetector.detect(@project_dir)
|
|
103
|
+
rescue => e
|
|
104
|
+
Aidp.log_warn("test_runner", "tooling_detection_failed", error: e.message)
|
|
105
|
+
Aidp::ToolingDetector::Result.new(test_commands: [], lint_commands: [])
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def log_fallback(type, commands)
|
|
109
|
+
Aidp.log_info(
|
|
110
|
+
"test_runner",
|
|
111
|
+
"auto_detected_commands",
|
|
112
|
+
category: type,
|
|
113
|
+
commands: commands
|
|
114
|
+
)
|
|
115
|
+
rescue NameError
|
|
116
|
+
# Logging infrastructure not available in some tests
|
|
117
|
+
end
|
|
81
118
|
end
|
|
82
119
|
end
|
|
83
120
|
end
|