aidp 0.19.1 → 0.20.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/lib/aidp/cli.rb +12 -0
- data/lib/aidp/harness/state/persistence.rb +36 -19
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -1
- data/lib/aidp/logger.rb +13 -1
- data/lib/aidp/rescue_logging.rb +20 -9
- data/lib/aidp/setup/wizard.rb +277 -56
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/guided_agent.rb +98 -13
- data/lib/aidp.rb +1 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 724d07296380ad69a72b1f9d6bf4dd44d77cf35f994eb35e1e0d224ed32e93a0
|
|
4
|
+
data.tar.gz: f54213c88e3e532205852f8dfbedcc0c264a446a65a1110ddf7de524f10e64d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f8788f4f8af45642b276e350f7d64e2413ae1532cbe02fe29c95c06c8fcbf84ecd66c35cb73e5b16637b6780f52ba77991b255d78f5f2464200a5db6acce262b
|
|
7
|
+
data.tar.gz: 2cd95f1e5369205332ad1f87d313b6dc99801d7bdb6f13d0b661cfbba4a771a9e8277e6e8f43e8234a2bea1a6ba23cc73ef54e480f2cf73788bc6dd17aa8de74
|
data/lib/aidp/cli.rb
CHANGED
|
@@ -146,6 +146,16 @@ module Aidp
|
|
|
146
146
|
|
|
147
147
|
class << self
|
|
148
148
|
extend Aidp::MessageDisplay::ClassMethods
|
|
149
|
+
extend Aidp::RescueLogging
|
|
150
|
+
|
|
151
|
+
# Explicit singleton delegator (defensive: ensure availability even if extend fails to attach)
|
|
152
|
+
def log_rescue(error, component:, action:, fallback: nil, level: :warn, **context)
|
|
153
|
+
Aidp::RescueLogging.log_rescue(error, component: component, action: action, fallback: fallback, level: level, **context)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Store last parsed options for access by UI components (e.g., verbose flag)
|
|
157
|
+
@last_options = nil
|
|
158
|
+
attr_accessor :last_options
|
|
149
159
|
|
|
150
160
|
def create_prompt
|
|
151
161
|
::TTY::Prompt.new
|
|
@@ -156,6 +166,7 @@ module Aidp
|
|
|
156
166
|
return run_subcommand(args) if subcommand?(args)
|
|
157
167
|
|
|
158
168
|
options = parse_options(args)
|
|
169
|
+
self.last_options = options
|
|
159
170
|
|
|
160
171
|
if options[:help]
|
|
161
172
|
display_message(options[:parser].to_s, type: :info)
|
|
@@ -324,6 +335,7 @@ module Aidp
|
|
|
324
335
|
opts.on("-h", "--help", "Show this help message") { options[:help] = true }
|
|
325
336
|
opts.on("-v", "--version", "Show version information") { options[:version] = true }
|
|
326
337
|
opts.on("--setup-config", "Setup or reconfigure config file") { options[:setup_config] = true }
|
|
338
|
+
opts.on("--verbose", "Show detailed prompts and raw provider responses during guided workflow") { options[:verbose] = true }
|
|
327
339
|
|
|
328
340
|
opts.separator ""
|
|
329
341
|
opts.separator "Examples:"
|
|
@@ -18,21 +18,27 @@ module Aidp
|
|
|
18
18
|
# Callers should set skip_persistence: true for test/dry-run scenarios
|
|
19
19
|
@skip_persistence = skip_persistence
|
|
20
20
|
ensure_state_directory
|
|
21
|
+
Aidp.log_debug("state_persistence", "initialized", mode: @mode, skip: @skip_persistence, dir: @state_dir)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def has_state?
|
|
24
25
|
return false if @skip_persistence
|
|
25
|
-
File.exist?(@state_file)
|
|
26
|
+
exists = File.exist?(@state_file)
|
|
27
|
+
Aidp.log_debug("state_persistence", "has_state?", exists: exists, file: @state_file) if exists
|
|
28
|
+
exists
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
def load_state
|
|
29
32
|
return {} if @skip_persistence || !has_state?
|
|
30
33
|
|
|
31
34
|
with_lock do
|
|
35
|
+
Aidp.log_debug("state_persistence", "load_state.start", file: @state_file)
|
|
32
36
|
content = File.read(@state_file)
|
|
33
|
-
JSON.parse(content, symbolize_names: true)
|
|
37
|
+
parsed = JSON.parse(content, symbolize_names: true)
|
|
38
|
+
Aidp.log_debug("state_persistence", "load_state.success", keys: parsed.keys.size, file: @state_file)
|
|
39
|
+
parsed
|
|
34
40
|
rescue JSON::ParserError => e
|
|
35
|
-
|
|
41
|
+
Aidp.log_warn("state_persistence", "parse_error", error: e.message, file: @state_file)
|
|
36
42
|
{}
|
|
37
43
|
end
|
|
38
44
|
end
|
|
@@ -41,8 +47,10 @@ module Aidp
|
|
|
41
47
|
return if @skip_persistence
|
|
42
48
|
|
|
43
49
|
with_lock do
|
|
50
|
+
Aidp.log_debug("state_persistence", "save_state.start", keys: state_data.keys.size)
|
|
44
51
|
state_with_metadata = add_metadata(state_data)
|
|
45
52
|
write_atomically(state_with_metadata)
|
|
53
|
+
Aidp.log_debug("state_persistence", "save_state.written", file: @state_file, size: state_with_metadata.keys.size)
|
|
46
54
|
end
|
|
47
55
|
end
|
|
48
56
|
|
|
@@ -50,7 +58,9 @@ module Aidp
|
|
|
50
58
|
return if @skip_persistence
|
|
51
59
|
|
|
52
60
|
with_lock do
|
|
61
|
+
Aidp.log_debug("state_persistence", "clear_state.start", file: @state_file)
|
|
53
62
|
File.delete(@state_file) if File.exist?(@state_file)
|
|
63
|
+
Aidp.log_debug("state_persistence", "clear_state.done", file: @state_file)
|
|
54
64
|
end
|
|
55
65
|
end
|
|
56
66
|
|
|
@@ -66,8 +76,10 @@ module Aidp
|
|
|
66
76
|
|
|
67
77
|
def write_atomically(state_with_metadata)
|
|
68
78
|
temp_file = "#{@state_file}.tmp"
|
|
79
|
+
Aidp.log_debug("state_persistence", "write_atomically.start", temp: temp_file)
|
|
69
80
|
File.write(temp_file, JSON.pretty_generate(state_with_metadata))
|
|
70
81
|
File.rename(temp_file, @state_file)
|
|
82
|
+
Aidp.log_debug("state_persistence", "write_atomically.rename", file: @state_file)
|
|
71
83
|
end
|
|
72
84
|
|
|
73
85
|
def ensure_state_directory
|
|
@@ -76,45 +88,50 @@ module Aidp
|
|
|
76
88
|
|
|
77
89
|
def with_lock(&block)
|
|
78
90
|
return yield if @skip_persistence
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
result = acquire_lock_with_timeout(&block)
|
|
92
|
+
result
|
|
81
93
|
ensure
|
|
82
94
|
cleanup_lock_file
|
|
83
95
|
end
|
|
84
96
|
|
|
85
97
|
def acquire_lock_with_timeout(&block)
|
|
86
|
-
|
|
87
|
-
timeout = 30
|
|
98
|
+
timeout = ENV["AIDP_STATE_LOCK_TIMEOUT"]&.to_f || ((ENV["RSPEC_RUNNING"] == "true") ? 1.0 : 30.0)
|
|
88
99
|
start_time = Time.now
|
|
89
|
-
|
|
100
|
+
attempt_result = nil
|
|
90
101
|
while (Time.now - start_time) < timeout
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
acquired, attempt_result = try_acquire_lock(&block)
|
|
103
|
+
return attempt_result if acquired
|
|
93
104
|
sleep_briefly
|
|
94
105
|
end
|
|
95
|
-
|
|
96
|
-
raise_lock_timeout_error unless lock_acquired
|
|
106
|
+
raise_lock_timeout_error(timeout)
|
|
97
107
|
end
|
|
98
108
|
|
|
99
109
|
def try_acquire_lock(&block)
|
|
100
110
|
File.open(@lock_file, File::CREAT | File::EXCL | File::WRONLY) do |_lock|
|
|
101
|
-
|
|
102
|
-
true
|
|
111
|
+
Aidp.log_debug("state_persistence", "lock.acquired", file: @lock_file)
|
|
112
|
+
[true, yield]
|
|
103
113
|
end
|
|
104
114
|
rescue Errno::EEXIST
|
|
105
|
-
|
|
115
|
+
Aidp.log_debug("state_persistence", "lock.busy", file: @lock_file)
|
|
116
|
+
[false, nil]
|
|
106
117
|
end
|
|
107
118
|
|
|
108
119
|
def sleep_briefly
|
|
109
|
-
sleep(0.
|
|
120
|
+
sleep(ENV["AIDP_STATE_LOCK_SLEEP"]&.to_f || 0.05)
|
|
110
121
|
end
|
|
111
122
|
|
|
112
|
-
def raise_lock_timeout_error
|
|
113
|
-
|
|
123
|
+
def raise_lock_timeout_error(timeout)
|
|
124
|
+
# Prefer explicit error class; fall back if not defined yet
|
|
125
|
+
error_class = defined?(Aidp::Errors::StateError) ? Aidp::Errors::StateError : RuntimeError
|
|
126
|
+
Aidp.log_error("state_persistence", "lock.timeout", file: @lock_file, waited: timeout)
|
|
127
|
+
raise error_class, "Could not acquire state lock within #{timeout} seconds"
|
|
114
128
|
end
|
|
115
129
|
|
|
116
130
|
def cleanup_lock_file
|
|
117
|
-
|
|
131
|
+
if File.exist?(@lock_file)
|
|
132
|
+
File.delete(@lock_file)
|
|
133
|
+
Aidp.log_debug("state_persistence", "lock.cleaned", file: @lock_file)
|
|
134
|
+
end
|
|
118
135
|
end
|
|
119
136
|
end
|
|
120
137
|
end
|
|
@@ -251,7 +251,10 @@ module Aidp
|
|
|
251
251
|
def select_guided_workflow
|
|
252
252
|
# Use the guided agent to help user select workflow
|
|
253
253
|
# Don't pass prompt so it uses EnhancedInput with full readline support
|
|
254
|
-
|
|
254
|
+
verbose_flag = (defined?(Aidp::CLI) && Aidp::CLI.respond_to?(:last_options) && Aidp::CLI.last_options) ? Aidp::CLI.last_options[:verbose] : false
|
|
255
|
+
# Fallback: store verbose in an env for easier access if options not available
|
|
256
|
+
verbose = verbose_flag || ENV["AIDP_VERBOSE"] == "1"
|
|
257
|
+
guided_agent = Aidp::Workflows::GuidedAgent.new(@project_dir, verbose: verbose)
|
|
255
258
|
result = guided_agent.select_workflow
|
|
256
259
|
|
|
257
260
|
# Store user input for later use
|
data/lib/aidp/logger.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Aidp
|
|
|
33
33
|
attr_reader :level, :json_format
|
|
34
34
|
|
|
35
35
|
def initialize(project_dir = Dir.pwd, config = {})
|
|
36
|
-
@project_dir = project_dir
|
|
36
|
+
@project_dir = sanitize_project_dir(project_dir)
|
|
37
37
|
@config = config
|
|
38
38
|
@level = determine_log_level
|
|
39
39
|
@json_format = config[:json] || false
|
|
@@ -195,6 +195,18 @@ module Aidp
|
|
|
195
195
|
def redact_hash(hash)
|
|
196
196
|
hash.transform_values { |v| v.is_a?(String) ? redact(v) : v }
|
|
197
197
|
end
|
|
198
|
+
|
|
199
|
+
# Guard against accidentally passing stream sentinel strings or invalid characters
|
|
200
|
+
# that would create odd top-level directories like "<STDERR>".
|
|
201
|
+
def sanitize_project_dir(dir)
|
|
202
|
+
return Dir.pwd if dir.nil?
|
|
203
|
+
str = dir.to_s
|
|
204
|
+
if str.empty? || str.match?(/[<>|]/) || str.match?(/[\x00-\x1F]/)
|
|
205
|
+
Kernel.warn "[AIDP Logger] Invalid project_dir '#{str}' - falling back to #{Dir.pwd}"
|
|
206
|
+
return Dir.pwd
|
|
207
|
+
end
|
|
208
|
+
str
|
|
209
|
+
end
|
|
198
210
|
end
|
|
199
211
|
|
|
200
212
|
# Module-level logger accessor
|
data/lib/aidp/rescue_logging.rb
CHANGED
|
@@ -13,24 +13,35 @@ module Aidp
|
|
|
13
13
|
# - includes error class, message
|
|
14
14
|
# - optional fallback and extra context hash merged in
|
|
15
15
|
module RescueLogging
|
|
16
|
+
# Instance-level helper (made public so extend works for singleton contexts)
|
|
16
17
|
def log_rescue(error, component:, action:, fallback: nil, level: :warn, **context)
|
|
18
|
+
Aidp::RescueLogging.__log_rescue_impl(self, error, component: component, action: action, fallback: fallback, level: level, **context)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Module-level access (Aidp::RescueLogging.log_rescue) for direct calls if desired
|
|
22
|
+
def self.log_rescue(error, component:, action:, fallback: nil, level: :warn, **context)
|
|
23
|
+
Aidp::RescueLogging.__log_rescue_impl(self, error, component: component, action: action, fallback: fallback, level: level, **context)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Internal implementation shared by instance & module forms
|
|
27
|
+
def self.__log_rescue_impl(context_object, error, component:, action:, fallback:, level:, **extra)
|
|
17
28
|
data = {
|
|
18
29
|
error_class: error.class.name,
|
|
19
30
|
error_message: error.message,
|
|
20
31
|
action: action
|
|
21
32
|
}
|
|
22
33
|
data[:fallback] = fallback if fallback
|
|
23
|
-
data.merge!(
|
|
34
|
+
data.merge!(extra) unless extra.empty?
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
begin
|
|
37
|
+
if context_object.respond_to?(:debug_log)
|
|
38
|
+
context_object.debug_log("⚠️ Rescue in #{component}: #{action}", level: level, data: data)
|
|
39
|
+
else
|
|
40
|
+
Aidp.logger.send(level, component, "Rescued exception during #{action}", **data)
|
|
41
|
+
end
|
|
42
|
+
rescue => logging_error
|
|
43
|
+
warn "[AIDP Rescue Logging Error] Failed to log rescue for #{component}:#{action} - #{error.class}: #{error.message} (logging error: #{logging_error.message})"
|
|
30
44
|
end
|
|
31
|
-
rescue => logging_error
|
|
32
|
-
# Last resort: avoid raising from logging path - fall back to STDERR
|
|
33
|
-
warn "[AIDP Rescue Logging Error] Failed to log rescue for #{component}:#{action} - #{error.class}: #{error.message} (logging error: #{logging_error.message})"
|
|
34
45
|
end
|
|
35
46
|
end
|
|
36
47
|
end
|
data/lib/aidp/setup/wizard.rb
CHANGED
|
@@ -30,6 +30,8 @@ module Aidp
|
|
|
30
30
|
|
|
31
31
|
def run
|
|
32
32
|
display_welcome
|
|
33
|
+
# Normalize any legacy or label-based model_family entries before prompting
|
|
34
|
+
normalize_existing_model_families!
|
|
33
35
|
return @saved if skip_wizard?
|
|
34
36
|
|
|
35
37
|
configure_providers
|
|
@@ -150,11 +152,30 @@ module Aidp
|
|
|
150
152
|
fallback_choices = available_providers.reject { |_, name| name == provider_choice }
|
|
151
153
|
fallback_default_names = existing_fallbacks.filter_map { |provider_name| fallback_choices.key(provider_name) }
|
|
152
154
|
|
|
155
|
+
prompt.say("\n💡 Use ↑/↓ arrows to navigate, SPACE to select/deselect, ENTER to confirm")
|
|
153
156
|
fallback_selected = prompt.multi_select("Select fallback providers (used if primary fails):", default: fallback_default_names) do |menu|
|
|
154
157
|
fallback_choices.each do |display_name, provider_name|
|
|
155
158
|
menu.choice display_name, provider_name
|
|
156
159
|
end
|
|
157
160
|
end
|
|
161
|
+
if ENV["AIDP_FALLBACK_DEBUG"] == "1"
|
|
162
|
+
prompt.say("[debug] raw multi_select fallback_selected=#{fallback_selected.inspect}")
|
|
163
|
+
end
|
|
164
|
+
# Recovery: if multi_select unexpectedly returns empty and there were no existing fallbacks, offer a single-select
|
|
165
|
+
if fallback_selected.empty? && existing_fallbacks.empty? && !fallback_choices.empty?
|
|
166
|
+
if ENV["AIDP_FALLBACK_DEBUG"] == "1"
|
|
167
|
+
prompt.say("[debug] invoking recovery single-select for first fallback")
|
|
168
|
+
end
|
|
169
|
+
if prompt.yes?("No fallback selected. Add one?", default: true)
|
|
170
|
+
recovery_choice = prompt.select("Select a fallback provider:") do |menu|
|
|
171
|
+
fallback_choices.each do |display_name, provider_name|
|
|
172
|
+
menu.choice display_name, provider_name
|
|
173
|
+
end
|
|
174
|
+
menu.choice "Skip", :skip
|
|
175
|
+
end
|
|
176
|
+
fallback_selected = [recovery_choice] unless recovery_choice == :skip
|
|
177
|
+
end
|
|
178
|
+
end
|
|
158
179
|
|
|
159
180
|
# If user selected none but we had existing fallbacks, confirm removal
|
|
160
181
|
if fallback_selected.empty? && existing_fallbacks.any?
|
|
@@ -167,10 +188,81 @@ module Aidp
|
|
|
167
188
|
set([:harness, :fallback_providers], cleaned_fallbacks)
|
|
168
189
|
|
|
169
190
|
# Auto-create minimal provider configs for fallbacks if missing
|
|
170
|
-
cleaned_fallbacks.each
|
|
191
|
+
cleaned_fallbacks.each do |fp|
|
|
192
|
+
prompt.say("[debug] ensuring billing config for fallback '#{fp}'") if ENV["AIDP_FALLBACK_DEBUG"] == "1"
|
|
193
|
+
ensure_provider_billing_config(fp, force: true)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Offer editing of existing provider configurations (primary + fallbacks)
|
|
197
|
+
# (editable will be recomputed after any additional fallback additions)
|
|
198
|
+
([provider_choice] + cleaned_fallbacks).uniq.reject { |p| p == "custom" }
|
|
199
|
+
|
|
200
|
+
# Optional: allow adding more fallbacks iteratively
|
|
201
|
+
if prompt.yes?("Add another fallback provider?", default: false)
|
|
202
|
+
loop do
|
|
203
|
+
remaining = available_providers.reject { |_, name| ([provider_choice] + cleaned_fallbacks).include?(name) }
|
|
204
|
+
break if remaining.empty?
|
|
205
|
+
add_choice = prompt.select("Select additional fallback provider:") do |menu|
|
|
206
|
+
remaining.each { |display, name| menu.choice display, name }
|
|
207
|
+
menu.choice "Done", :done
|
|
208
|
+
end
|
|
209
|
+
break if add_choice == :done
|
|
210
|
+
unless cleaned_fallbacks.include?(add_choice)
|
|
211
|
+
cleaned_fallbacks << add_choice
|
|
212
|
+
set([:harness, :fallback_providers], cleaned_fallbacks)
|
|
213
|
+
ensure_provider_billing_config(add_choice, force: true)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
# Recompute editable after additions
|
|
218
|
+
editable = ([provider_choice] + cleaned_fallbacks).uniq.reject { |p| p == "custom" }
|
|
219
|
+
if editable.any? && prompt.yes?("Edit provider configuration details (billing/model family)?", default: false)
|
|
220
|
+
loop do
|
|
221
|
+
# Build dynamic mapping of display names -> internal names for edit menu
|
|
222
|
+
available_map = discover_available_providers # {display_name => internal_name}
|
|
223
|
+
display_name_for = available_map.invert # {internal_name => display_name}
|
|
224
|
+
to_edit = prompt.select("Select a provider to edit or add:") do |menu|
|
|
225
|
+
editable.each do |prov|
|
|
226
|
+
display_label = display_name_for.fetch(prov, prov.capitalize)
|
|
227
|
+
menu.choice display_label, prov
|
|
228
|
+
end
|
|
229
|
+
# Sentinel option: add a new fallback provider that isn't yet in editable list
|
|
230
|
+
remaining = available_map.values - editable
|
|
231
|
+
if remaining.any?
|
|
232
|
+
menu.choice "➕ Add fallback provider…", :add_fallback
|
|
233
|
+
end
|
|
234
|
+
menu.choice "Done", :done
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
case to_edit
|
|
238
|
+
when :done
|
|
239
|
+
break
|
|
240
|
+
when :add_fallback
|
|
241
|
+
# Allow user to pick from remaining providers by display name
|
|
242
|
+
remaining_map = available_map.select { |disp, internal| !editable.include?(internal) && internal != provider_choice }
|
|
243
|
+
add_choice = prompt.select("Select provider to add as fallback:") do |menu|
|
|
244
|
+
remaining_map.each { |disp, internal| menu.choice disp, internal }
|
|
245
|
+
menu.choice "Cancel", :cancel
|
|
246
|
+
end
|
|
247
|
+
next if add_choice == :cancel
|
|
248
|
+
unless cleaned_fallbacks.include?(add_choice)
|
|
249
|
+
cleaned_fallbacks << add_choice
|
|
250
|
+
set([:harness, :fallback_providers], cleaned_fallbacks)
|
|
251
|
+
prompt.say("[debug] ensuring billing config for newly added fallback '#{add_choice}'") if ENV["AIDP_FALLBACK_DEBUG"] == "1"
|
|
252
|
+
ensure_provider_billing_config(add_choice, force: true)
|
|
253
|
+
editable = ([provider_choice] + cleaned_fallbacks).uniq.reject { |p| p == "custom" }
|
|
254
|
+
end
|
|
255
|
+
else
|
|
256
|
+
edit_provider_configuration(to_edit)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
171
260
|
|
|
172
261
|
# Provide informational note (no secret handling stored)
|
|
173
262
|
show_provider_info_note(provider_choice) unless provider_choice == "custom"
|
|
263
|
+
|
|
264
|
+
# Show summary of configured providers (replaces the earlier inline summary)
|
|
265
|
+
show_provider_summary(provider_choice, cleaned_fallbacks) unless provider_choice == "custom"
|
|
174
266
|
end
|
|
175
267
|
|
|
176
268
|
# Removed MCP configuration step (MCP now expected to be provider-specific if used)
|
|
@@ -262,13 +354,19 @@ module Aidp
|
|
|
262
354
|
enabled = prompt.yes?("Enable coverage tracking?", default: existing.fetch(:enabled, false))
|
|
263
355
|
return set([:work_loop, :coverage], {enabled: false}) unless enabled
|
|
264
356
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
357
|
+
coverage_tool_choices = [
|
|
358
|
+
["SimpleCov (Ruby)", "simplecov"],
|
|
359
|
+
["NYC/Istanbul (JavaScript)", "nyc"],
|
|
360
|
+
["Coverage.py (Python)", "coverage.py"],
|
|
361
|
+
["go test -cover (Go)", "go-cover"],
|
|
362
|
+
["Jest (JavaScript)", "jest"],
|
|
363
|
+
["Other", "other"]
|
|
364
|
+
]
|
|
365
|
+
coverage_tool_default = existing[:tool]
|
|
366
|
+
coverage_tool_default_label = coverage_tool_choices.find { |label, value| value == coverage_tool_default }&.first
|
|
367
|
+
|
|
368
|
+
tool = prompt.select("Which coverage tool do you use?", default: coverage_tool_default_label) do |menu|
|
|
369
|
+
coverage_tool_choices.each { |label, value| menu.choice label, value }
|
|
272
370
|
end
|
|
273
371
|
|
|
274
372
|
run_command = ask_with_default("Coverage run command", existing[:run_command] || detect_coverage_command(tool))
|
|
@@ -300,10 +398,16 @@ module Aidp
|
|
|
300
398
|
enabled = prompt.yes?("Enable interactive testing tools?", default: existing.fetch(:enabled, false))
|
|
301
399
|
return set([:work_loop, :interactive_testing], {enabled: false}) unless enabled
|
|
302
400
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
401
|
+
app_type_choices = [
|
|
402
|
+
["Web application", "web"],
|
|
403
|
+
["CLI application", "cli"],
|
|
404
|
+
["Desktop application", "desktop"]
|
|
405
|
+
]
|
|
406
|
+
app_type_default = existing[:app_type]
|
|
407
|
+
app_type_default_label = app_type_choices.find { |label, value| value == app_type_default }&.first
|
|
408
|
+
|
|
409
|
+
app_type = prompt.select("What type of application are you testing?", default: app_type_default_label) do |menu|
|
|
410
|
+
app_type_choices.each { |label, value| menu.choice label, value }
|
|
307
411
|
end
|
|
308
412
|
|
|
309
413
|
tools = {}
|
|
@@ -382,17 +486,21 @@ module Aidp
|
|
|
382
486
|
|
|
383
487
|
# Detect VCS
|
|
384
488
|
detected_vcs = detect_vcs_tool
|
|
489
|
+
vcs_choices = [
|
|
490
|
+
["git", "git"],
|
|
491
|
+
["svn", "svn"],
|
|
492
|
+
["none (no VCS)", "none"]
|
|
493
|
+
]
|
|
494
|
+
vcs_default = existing[:tool] || detected_vcs || "git"
|
|
495
|
+
vcs_default_label = vcs_choices.find { |label, value| value == vcs_default }&.first
|
|
496
|
+
|
|
385
497
|
vcs_tool = if detected_vcs
|
|
386
|
-
prompt.select("Detected #{detected_vcs}. Use this version control system?", default:
|
|
387
|
-
menu.choice
|
|
388
|
-
menu.choice "svn", "svn"
|
|
389
|
-
menu.choice "none (no VCS)", "none"
|
|
498
|
+
prompt.select("Detected #{detected_vcs}. Use this version control system?", default: vcs_default_label) do |menu|
|
|
499
|
+
vcs_choices.each { |label, value| menu.choice label, value }
|
|
390
500
|
end
|
|
391
501
|
else
|
|
392
|
-
prompt.select("Which version control system do you use?", default:
|
|
393
|
-
menu.choice
|
|
394
|
-
menu.choice "svn", "svn"
|
|
395
|
-
menu.choice "none (no VCS)", "none"
|
|
502
|
+
prompt.select("Which version control system do you use?", default: vcs_default_label) do |menu|
|
|
503
|
+
vcs_choices.each { |label, value| menu.choice label, value }
|
|
396
504
|
end
|
|
397
505
|
end
|
|
398
506
|
|
|
@@ -400,10 +508,18 @@ module Aidp
|
|
|
400
508
|
|
|
401
509
|
prompt.say("\n📋 Commit Behavior (applies to copilot/interactive mode only)")
|
|
402
510
|
prompt.say("Note: Watch mode and fully automatic daemon mode will always commit changes.")
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
511
|
+
|
|
512
|
+
# Map value defaults to choice labels for TTY::Prompt validation
|
|
513
|
+
behavior_choices = [
|
|
514
|
+
["Do nothing (manual git operations)", "nothing"],
|
|
515
|
+
["Stage changes only", "stage"],
|
|
516
|
+
["Stage and commit changes", "commit"]
|
|
517
|
+
]
|
|
518
|
+
behavior_default = existing[:behavior] || "nothing"
|
|
519
|
+
behavior_default_label = behavior_choices.find { |label, value| value == behavior_default }&.first
|
|
520
|
+
|
|
521
|
+
behavior = prompt.select("In copilot mode, should aidp:", default: behavior_default_label) do |menu|
|
|
522
|
+
behavior_choices.each { |label, value| menu.choice label, value }
|
|
407
523
|
end
|
|
408
524
|
|
|
409
525
|
# Commit message configuration
|
|
@@ -437,10 +553,16 @@ module Aidp
|
|
|
437
553
|
|
|
438
554
|
# Commit message style
|
|
439
555
|
commit_style = if conventional_commits
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
556
|
+
commit_style_choices = [
|
|
557
|
+
["Default (e.g., 'feat: add user authentication')", "default"],
|
|
558
|
+
["Angular (with scope: 'feat(auth): add login')", "angular"],
|
|
559
|
+
["Emoji (e.g., '✨ feat: add user authentication')", "emoji"]
|
|
560
|
+
]
|
|
561
|
+
commit_style_default = existing[:commit_style] || "default"
|
|
562
|
+
commit_style_default_label = commit_style_choices.find { |label, value| value == commit_style_default }&.first
|
|
563
|
+
|
|
564
|
+
prompt.select("Conventional commit style:", default: commit_style_default_label) do |menu|
|
|
565
|
+
commit_style_choices.each { |label, value| menu.choice label, value }
|
|
444
566
|
end
|
|
445
567
|
else
|
|
446
568
|
"default"
|
|
@@ -476,10 +598,16 @@ module Aidp
|
|
|
476
598
|
)
|
|
477
599
|
|
|
478
600
|
if auto_create_pr
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
601
|
+
pr_strategy_choices = [
|
|
602
|
+
["Create as draft PR (safe, allows review before merge)", "draft"],
|
|
603
|
+
["Create as ready PR (immediately reviewable)", "ready"],
|
|
604
|
+
["Create and auto-merge (fully autonomous, requires approval rules)", "auto_merge"]
|
|
605
|
+
]
|
|
606
|
+
pr_strategy_default = existing[:pr_strategy] || "draft"
|
|
607
|
+
pr_strategy_default_label = pr_strategy_choices.find { |label, value| value == pr_strategy_default }&.first
|
|
608
|
+
|
|
609
|
+
pr_strategy = prompt.select("PR creation strategy:", default: pr_strategy_default_label) do |menu|
|
|
610
|
+
pr_strategy_choices.each { |label, value| menu.choice label, value }
|
|
483
611
|
end
|
|
484
612
|
|
|
485
613
|
{
|
|
@@ -614,11 +742,16 @@ module Aidp
|
|
|
614
742
|
prompt.say("-" * 40)
|
|
615
743
|
existing = get([:logging]) || {}
|
|
616
744
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
745
|
+
log_level_choices = [
|
|
746
|
+
["Debug", "debug"],
|
|
747
|
+
["Info", "info"],
|
|
748
|
+
["Error", "error"]
|
|
749
|
+
]
|
|
750
|
+
log_level_default = existing[:level] || "info"
|
|
751
|
+
log_level_default_label = log_level_choices.find { |label, value| value == log_level_default }&.first
|
|
752
|
+
|
|
753
|
+
log_level = prompt.select("Log level:", default: log_level_default_label) do |menu|
|
|
754
|
+
log_level_choices.each { |label, value| menu.choice label, value }
|
|
622
755
|
end
|
|
623
756
|
json = prompt.yes?("Use JSON log format?", default: existing.fetch(:json, false))
|
|
624
757
|
max_size = ask_with_default("Max log size (MB)", (existing[:max_size_mb] || 10).to_s) { |value| value.to_i }
|
|
@@ -897,44 +1030,132 @@ module Aidp
|
|
|
897
1030
|
prompt.say("Only the billing model (subscription vs usage_based) is recorded for fallback decisions.")
|
|
898
1031
|
end
|
|
899
1032
|
|
|
1033
|
+
def show_provider_summary(primary, fallbacks)
|
|
1034
|
+
prompt.say("\n📋 Provider Configuration Summary:")
|
|
1035
|
+
providers_config = get([:providers]) || {}
|
|
1036
|
+
|
|
1037
|
+
# Show primary
|
|
1038
|
+
if primary && primary != "custom"
|
|
1039
|
+
primary_cfg = providers_config[primary.to_sym] || {}
|
|
1040
|
+
prompt.say(" ✓ Primary: #{primary} (#{primary_cfg[:type] || "not configured"}, #{primary_cfg[:model_family] || "auto"})")
|
|
1041
|
+
end
|
|
1042
|
+
|
|
1043
|
+
# Show fallbacks
|
|
1044
|
+
if fallbacks && !fallbacks.empty?
|
|
1045
|
+
fallbacks.each do |fallback|
|
|
1046
|
+
fallback_cfg = providers_config[fallback.to_sym] || {}
|
|
1047
|
+
prompt.say(" ✓ Fallback: #{fallback} (#{fallback_cfg[:type] || "not configured"}, #{fallback_cfg[:model_family] || "auto"})")
|
|
1048
|
+
end
|
|
1049
|
+
end
|
|
1050
|
+
end
|
|
1051
|
+
|
|
900
1052
|
# Ensure a minimal billing configuration exists for a selected provider (no secrets)
|
|
901
|
-
def ensure_provider_billing_config(provider_name)
|
|
1053
|
+
def ensure_provider_billing_config(provider_name, force: false)
|
|
902
1054
|
return if provider_name.nil? || provider_name == "custom"
|
|
903
1055
|
providers_section = get([:providers]) || {}
|
|
904
1056
|
existing = providers_section[provider_name.to_sym]
|
|
905
1057
|
|
|
906
|
-
if existing && existing[:type]
|
|
1058
|
+
if existing && existing[:type] && !force
|
|
907
1059
|
prompt.say(" • Provider '#{provider_name}' already configured (type: #{existing[:type]})")
|
|
908
|
-
# Still ask for model family if not set
|
|
909
1060
|
unless existing[:model_family]
|
|
910
|
-
model_family = ask_model_family(provider_name
|
|
1061
|
+
model_family = ask_model_family(provider_name)
|
|
911
1062
|
set([:providers, provider_name.to_sym, :model_family], model_family)
|
|
912
1063
|
end
|
|
913
1064
|
return
|
|
914
1065
|
end
|
|
915
1066
|
|
|
916
|
-
provider_type =
|
|
917
|
-
model_family = ask_model_family(provider_name)
|
|
918
|
-
|
|
919
|
-
|
|
1067
|
+
provider_type = ask_provider_billing_type_with_default(provider_name, existing&.dig(:type))
|
|
1068
|
+
model_family = ask_model_family(provider_name, existing&.dig(:model_family) || "auto")
|
|
1069
|
+
merged = (existing || {}).merge(type: provider_type, model_family: model_family)
|
|
1070
|
+
set([:providers, provider_name.to_sym], merged)
|
|
1071
|
+
normalize_existing_model_families!
|
|
1072
|
+
action_word = if existing
|
|
1073
|
+
force ? "reconfigured" : "updated"
|
|
1074
|
+
else
|
|
1075
|
+
"added"
|
|
1076
|
+
end
|
|
1077
|
+
# Enhance messaging with display name when available
|
|
1078
|
+
display_name = discover_available_providers.invert.fetch(provider_name, provider_name)
|
|
1079
|
+
prompt.say(" • #{action_word.capitalize} provider '#{display_name}' (#{provider_name}) with billing type '#{provider_type}' and model family '#{model_family}'")
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
def edit_provider_configuration(provider_name)
|
|
1083
|
+
existing = get([:providers, provider_name.to_sym]) || {}
|
|
1084
|
+
prompt.say("\n🔧 Editing provider '#{provider_name}' (current: type=#{existing[:type] || "unset"}, model_family=#{existing[:model_family] || "unset"})")
|
|
1085
|
+
new_type = ask_provider_billing_type_with_default(provider_name, existing[:type])
|
|
1086
|
+
new_family = ask_model_family(provider_name, existing[:model_family] || "auto")
|
|
1087
|
+
set([:providers, provider_name.to_sym], {type: new_type, model_family: new_family})
|
|
1088
|
+
# Normalize immediately so tests relying on canonical value see 'claude' rather than label
|
|
1089
|
+
normalize_existing_model_families!
|
|
1090
|
+
prompt.ok("Updated '#{provider_name}' → type=#{new_type}, model_family=#{new_family}")
|
|
920
1091
|
end
|
|
921
1092
|
|
|
922
1093
|
def ask_provider_billing_type(provider_name)
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1094
|
+
ask_provider_billing_type_with_default(provider_name, nil)
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
BILLING_TYPE_CHOICES = [
|
|
1098
|
+
["Subscription / flat-rate", "subscription"],
|
|
1099
|
+
["Usage-based / metered (API)", "usage_based"],
|
|
1100
|
+
["Passthrough / local (no billing)", "passthrough"]
|
|
1101
|
+
].freeze
|
|
1102
|
+
|
|
1103
|
+
def ask_provider_billing_type_with_default(provider_name, default_value)
|
|
1104
|
+
default_label = BILLING_TYPE_CHOICES.find { |label, value| value == default_value }&.first
|
|
1105
|
+
suffix = default_value ? " (current: #{default_value})" : ""
|
|
1106
|
+
prompt.select("Billing model for #{provider_name}:#{suffix}", default: default_label) do |menu|
|
|
1107
|
+
BILLING_TYPE_CHOICES.each do |label, value|
|
|
1108
|
+
menu.choice(label, value)
|
|
1109
|
+
end
|
|
928
1110
|
end
|
|
929
1111
|
end
|
|
930
1112
|
|
|
1113
|
+
MODEL_FAMILY_CHOICES = [
|
|
1114
|
+
["Auto (let provider decide)", "auto"],
|
|
1115
|
+
["OpenAI o-series (reasoning models)", "openai_o"],
|
|
1116
|
+
["Anthropic Claude (balanced)", "claude"],
|
|
1117
|
+
["Mistral (European/open)", "mistral"],
|
|
1118
|
+
["Local LLM (self-hosted)", "local"]
|
|
1119
|
+
].freeze
|
|
1120
|
+
|
|
931
1121
|
def ask_model_family(provider_name, default = "auto")
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1122
|
+
# TTY::Prompt validates defaults against the displayed choice labels, not values.
|
|
1123
|
+
# Map the value default (e.g. "auto") to its corresponding label.
|
|
1124
|
+
default_label = MODEL_FAMILY_CHOICES.find { |label, value| value == default }&.first
|
|
1125
|
+
|
|
1126
|
+
prompt.select("Preferred model family for #{provider_name}:", default: default_label) do |menu|
|
|
1127
|
+
MODEL_FAMILY_CHOICES.each do |label, value|
|
|
1128
|
+
menu.choice(label, value)
|
|
1129
|
+
end
|
|
1130
|
+
end
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
# Canonicalization helpers ------------------------------------------------
|
|
1134
|
+
MODEL_FAMILY_LABEL_TO_VALUE = MODEL_FAMILY_CHOICES.each_with_object({}) do |(label, value), h|
|
|
1135
|
+
h[label] = value
|
|
1136
|
+
end.freeze
|
|
1137
|
+
MODEL_FAMILY_VALUES = MODEL_FAMILY_CHOICES.map { |(_, value)| value }.freeze
|
|
1138
|
+
|
|
1139
|
+
def normalize_model_family(value)
|
|
1140
|
+
return "auto" if value.nil? || value.to_s.strip.empty?
|
|
1141
|
+
# Already a canonical value
|
|
1142
|
+
return value if MODEL_FAMILY_VALUES.include?(value)
|
|
1143
|
+
# Try label -> value
|
|
1144
|
+
mapped = MODEL_FAMILY_LABEL_TO_VALUE[value]
|
|
1145
|
+
return mapped if mapped
|
|
1146
|
+
# Unknown legacy entry -> fallback to auto
|
|
1147
|
+
"auto"
|
|
1148
|
+
end
|
|
1149
|
+
|
|
1150
|
+
def normalize_existing_model_families!
|
|
1151
|
+
providers_cfg = @config[:providers]
|
|
1152
|
+
return unless providers_cfg.is_a?(Hash)
|
|
1153
|
+
providers_cfg.each do |prov_name, prov_cfg|
|
|
1154
|
+
next unless prov_cfg.is_a?(Hash)
|
|
1155
|
+
mf = prov_cfg[:model_family]
|
|
1156
|
+
# Normalize and write back only if different to avoid unnecessary YAML churn
|
|
1157
|
+
normalized = normalize_model_family(mf)
|
|
1158
|
+
prov_cfg[:model_family] = normalized
|
|
938
1159
|
end
|
|
939
1160
|
end
|
|
940
1161
|
|
data/lib/aidp/version.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Aidp
|
|
|
18
18
|
|
|
19
19
|
class ConversationError < StandardError; end
|
|
20
20
|
|
|
21
|
-
def initialize(project_dir, prompt: nil, use_enhanced_input: true)
|
|
21
|
+
def initialize(project_dir, prompt: nil, use_enhanced_input: true, verbose: false)
|
|
22
22
|
@project_dir = project_dir
|
|
23
23
|
|
|
24
24
|
# Use EnhancedInput with Reline for full readline-style key bindings
|
|
@@ -32,6 +32,9 @@ module Aidp
|
|
|
32
32
|
@provider_manager = Aidp::Harness::ProviderManager.new(@config_manager, prompt: @prompt)
|
|
33
33
|
@conversation_history = []
|
|
34
34
|
@user_input = {}
|
|
35
|
+
@invalid_planning_responses = 0
|
|
36
|
+
@verbose = verbose
|
|
37
|
+
@debug_env = ENV["DEBUG"] == "1" || ENV["DEBUG"] == "2"
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
# Main entry point for guided workflow selection
|
|
@@ -79,6 +82,7 @@ module Aidp
|
|
|
79
82
|
iteration += 1
|
|
80
83
|
# Ask AI for next question based on current plan
|
|
81
84
|
question_response = get_planning_questions(plan)
|
|
85
|
+
emit_verbose_iteration(plan, question_response)
|
|
82
86
|
|
|
83
87
|
# Debug: show raw provider response and parsed result
|
|
84
88
|
debug_log("Planning iteration #{iteration} provider response", level: :debug, data: {
|
|
@@ -131,9 +135,10 @@ module Aidp
|
|
|
131
135
|
end
|
|
132
136
|
|
|
133
137
|
response = call_provider_for_analysis(system_prompt, user_prompt)
|
|
134
|
-
parsed =
|
|
138
|
+
parsed = safe_parse_planning_response(response)
|
|
135
139
|
# Attach raw response for debug
|
|
136
140
|
parsed[:raw_response] = response
|
|
141
|
+
emit_verbose_raw_prompt(system_prompt, user_prompt, response)
|
|
137
142
|
parsed
|
|
138
143
|
end
|
|
139
144
|
|
|
@@ -239,16 +244,64 @@ module Aidp
|
|
|
239
244
|
|
|
240
245
|
if classified && attempts < max_attempts
|
|
241
246
|
display_message("⚠️ Provider '#{provider_name}' #{classified.tr("_", " ")} – attempting fallback...", type: :warning)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
if @provider_manager.respond_to?(:switch_provider_for_error)
|
|
248
|
+
switched = @provider_manager.switch_provider_for_error(classified, stderr: message)
|
|
249
|
+
if switched && switched != provider_name
|
|
250
|
+
display_message("↩️ Switched to provider '#{switched}'", type: :info)
|
|
251
|
+
retry
|
|
252
|
+
elsif switched == provider_name
|
|
253
|
+
# ProviderManager could not advance; mark current as rate limited to encourage next attempt to move on.
|
|
254
|
+
Aidp.logger.debug("guided_agent", "provider_switch_noop", provider: provider_name, reason: classified)
|
|
255
|
+
if @provider_manager.respond_to?(:mark_rate_limited)
|
|
256
|
+
@provider_manager.mark_rate_limited(provider_name)
|
|
257
|
+
next_provider = @provider_manager.switch_provider("rate_limit_forced", previous_error: message)
|
|
258
|
+
if next_provider && next_provider != provider_name
|
|
259
|
+
display_message("↩️ Switched to provider '#{next_provider}' (forced)", type: :info)
|
|
260
|
+
retry
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
246
264
|
end
|
|
247
265
|
end
|
|
248
266
|
raise
|
|
249
267
|
end
|
|
250
268
|
end
|
|
251
269
|
|
|
270
|
+
# Verbose output helpers
|
|
271
|
+
def emit_verbose_raw_prompt(system_prompt, user_prompt, raw_response)
|
|
272
|
+
return unless @verbose || @debug_env
|
|
273
|
+
if @verbose
|
|
274
|
+
display_message("\n--- Prompt Sent (Planning) ---", type: :muted)
|
|
275
|
+
display_message(system_prompt.strip, type: :muted)
|
|
276
|
+
display_message(user_prompt.strip, type: :muted)
|
|
277
|
+
display_message("--- Raw Provider Response ---", type: :muted)
|
|
278
|
+
display_message(raw_response.to_s.strip, type: :muted)
|
|
279
|
+
display_message("------------------------------\n", type: :muted)
|
|
280
|
+
elsif @debug_env
|
|
281
|
+
Aidp.logger.debug("guided_agent", "planning_prompt", system: system_prompt.strip, user: user_prompt.strip)
|
|
282
|
+
Aidp.logger.debug("guided_agent", "planning_raw_response", raw: raw_response.to_s.strip)
|
|
283
|
+
end
|
|
284
|
+
rescue => e
|
|
285
|
+
Aidp.logger.warn("guided_agent", "Failed verbose prompt emit", error: e.message)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def emit_verbose_iteration(plan, question_response)
|
|
289
|
+
return unless @verbose || @debug_env
|
|
290
|
+
summary = {complete: question_response[:complete], questions: question_response[:questions], reasoning: question_response[:reasoning], error: question_response[:error]}
|
|
291
|
+
if @verbose
|
|
292
|
+
display_message("\n=== Planning Iteration Summary ===", type: :info)
|
|
293
|
+
display_message("Questions: #{(summary[:questions] || []).join(" | ")}", type: :info)
|
|
294
|
+
display_message("Complete? #{summary[:complete]}", type: :info)
|
|
295
|
+
display_message("Reasoning: #{summary[:reasoning]}", type: :muted) if summary[:reasoning]
|
|
296
|
+
display_message("Error: #{summary[:error]}", type: :warning) if summary[:error]
|
|
297
|
+
display_message("=================================", type: :info)
|
|
298
|
+
elsif @debug_env
|
|
299
|
+
Aidp.logger.debug("guided_agent", "iteration_summary", summary: summary, plan_progress_keys: plan.keys)
|
|
300
|
+
end
|
|
301
|
+
rescue => e
|
|
302
|
+
Aidp.logger.warn("guided_agent", "Failed verbose iteration emit", error: e.message)
|
|
303
|
+
end
|
|
304
|
+
|
|
252
305
|
def validate_provider_configuration!
|
|
253
306
|
configured = @provider_manager.configured_providers
|
|
254
307
|
if configured.nil? || configured.empty?
|
|
@@ -307,20 +360,53 @@ module Aidp
|
|
|
307
360
|
json_match = response_text.match(/```json\s*(\{.*?\})\s*```/m) ||
|
|
308
361
|
response_text.match(/(\{.*\})/m)
|
|
309
362
|
|
|
310
|
-
unless json_match
|
|
311
|
-
return {complete: false, questions: ["Could you tell me more about your requirements?"]}
|
|
312
|
-
end
|
|
313
|
-
|
|
363
|
+
return {error: :invalid_format} unless json_match
|
|
314
364
|
JSON.parse(json_match[1], symbolize_names: true)
|
|
315
365
|
rescue JSON::ParserError
|
|
316
|
-
{
|
|
366
|
+
{error: :invalid_format}
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Provides structured fallback sequence when provider keeps returning invalid planning JSON.
|
|
370
|
+
# After exceeding sequence length, switches to manual entry question.
|
|
371
|
+
def safe_parse_planning_response(response_text)
|
|
372
|
+
parsed = parse_planning_response(response_text)
|
|
373
|
+
return parsed unless parsed.is_a?(Hash) && parsed[:error] == :invalid_format
|
|
374
|
+
|
|
375
|
+
@invalid_planning_responses += 1
|
|
376
|
+
fallback_sequence = [
|
|
377
|
+
"Provide scope (key features) and primary users.",
|
|
378
|
+
"List 3-5 key functional requirements and any technical constraints.",
|
|
379
|
+
"Supply any non-functional requirements (performance/security) or type 'skip'."
|
|
380
|
+
]
|
|
381
|
+
|
|
382
|
+
if @invalid_planning_responses <= fallback_sequence.size
|
|
383
|
+
{complete: false, questions: [fallback_sequence[@invalid_planning_responses - 1]], reasoning: "Fallback due to invalid provider response (format)", error: :fallback}
|
|
384
|
+
else
|
|
385
|
+
display_message("[ERROR] Provider returned invalid planning JSON #{@invalid_planning_responses} times. Enter combined plan details manually.", type: :error)
|
|
386
|
+
{complete: false, questions: ["Enter plan details manually (features; users; requirements; constraints) or type 'skip'"], reasoning: "Manual recovery mode", error: :manual_recovery}
|
|
387
|
+
end
|
|
317
388
|
end
|
|
318
389
|
|
|
319
390
|
def update_plan_from_answer(plan, question, answer)
|
|
320
391
|
# Simple heuristic-based plan updates
|
|
321
392
|
# In a more sophisticated implementation, use AI to categorize answers
|
|
322
393
|
|
|
323
|
-
|
|
394
|
+
# IMPORTANT: Check manual recovery sentinel prompt first so it isn't misclassified
|
|
395
|
+
# by broader keyword heuristics (e.g., it contains the word 'users').
|
|
396
|
+
if question.start_with?("Enter plan details manually")
|
|
397
|
+
unless answer.strip.downcase == "skip"
|
|
398
|
+
parts = answer.split(/;|\|/).map(&:strip).reject(&:empty?)
|
|
399
|
+
features, users, requirements, constraints = parts
|
|
400
|
+
plan[:scope][:included] ||= []
|
|
401
|
+
plan[:scope][:included] << features if features
|
|
402
|
+
plan[:users][:personas] ||= []
|
|
403
|
+
plan[:users][:personas] << users if users
|
|
404
|
+
plan[:requirements][:functional] ||= []
|
|
405
|
+
plan[:requirements][:functional] << requirements if requirements
|
|
406
|
+
plan[:constraints][:technical] ||= []
|
|
407
|
+
plan[:constraints][:technical] << constraints if constraints
|
|
408
|
+
end
|
|
409
|
+
elsif question.downcase.include?("scope") || question.downcase.include?("include")
|
|
324
410
|
plan[:scope][:included] ||= []
|
|
325
411
|
plan[:scope][:included] << answer
|
|
326
412
|
elsif question.downcase.include?("user") || question.downcase.include?("who")
|
|
@@ -338,7 +424,6 @@ module Aidp
|
|
|
338
424
|
elsif question.downcase.include?("complete") || question.downcase.include?("done") || question.downcase.include?("success")
|
|
339
425
|
plan[:completion_criteria] << answer
|
|
340
426
|
else
|
|
341
|
-
# General information
|
|
342
427
|
plan[:additional_context] ||= []
|
|
343
428
|
plan[:additional_context] << {question: question, answer: answer}
|
|
344
429
|
end
|
data/lib/aidp.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "aidp/core_ext/class_attribute"
|
|
|
7
7
|
require_relative "aidp/version"
|
|
8
8
|
require_relative "aidp/config"
|
|
9
9
|
require_relative "aidp/util"
|
|
10
|
+
require_relative "aidp/rescue_logging"
|
|
10
11
|
require_relative "aidp/message_display"
|
|
11
12
|
require_relative "aidp/concurrency"
|
|
12
13
|
require_relative "aidp/setup/wizard"
|