aidp 0.14.1 ā 0.14.2
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 +33 -0
- data/lib/aidp/debug_mixin.rb +34 -33
- data/lib/aidp/execute/async_work_loop_runner.rb +5 -0
- data/lib/aidp/execute/checkpoint.rb +28 -5
- data/lib/aidp/execute/interactive_repl.rb +7 -0
- data/lib/aidp/execute/work_loop_runner.rb +8 -1
- data/lib/aidp/harness/enhanced_runner.rb +2 -0
- data/lib/aidp/harness/provider_info.rb +14 -4
- data/lib/aidp/harness/provider_manager.rb +64 -12
- data/lib/aidp/jobs/background_runner.rb +10 -3
- data/lib/aidp/logger.rb +10 -71
- data/lib/aidp/providers/base.rb +2 -0
- data/lib/aidp/providers/github_copilot.rb +12 -0
- data/lib/aidp/rescue_logging.rb +36 -0
- data/lib/aidp/setup/wizard.rb +42 -46
- data/lib/aidp/storage/csv_storage.rb +33 -7
- data/lib/aidp/storage/json_storage.rb +33 -10
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/guided_agent.rb +95 -22
- metadata +2 -2
- data/lib/aidp/debug_logger.rb +0 -195
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
require "tty-prompt"
|
4
4
|
require_relative "provider_factory"
|
5
|
+
require_relative "../rescue_logging"
|
5
6
|
|
6
7
|
module Aidp
|
7
8
|
module Harness
|
8
9
|
# Manages provider switching and fallback logic
|
9
10
|
class ProviderManager
|
10
11
|
include Aidp::MessageDisplay
|
12
|
+
include Aidp::RescueLogging
|
11
13
|
|
12
14
|
def initialize(configuration, prompt: TTY::Prompt.new)
|
13
15
|
@configuration = configuration
|
@@ -69,6 +71,8 @@ module Aidp
|
|
69
71
|
|
70
72
|
# Switch to next available provider with sophisticated fallback logic
|
71
73
|
def switch_provider(reason = "manual_switch", context = {})
|
74
|
+
Aidp.logger.info("provider_manager", "Attempting provider switch", reason: reason, current: current_provider, **context)
|
75
|
+
|
72
76
|
# Get fallback chain for current provider
|
73
77
|
provider_fallback_chain = fallback_chain(current_provider)
|
74
78
|
|
@@ -79,7 +83,10 @@ module Aidp
|
|
79
83
|
success = set_current_provider(next_provider, reason, context)
|
80
84
|
if success
|
81
85
|
log_provider_switch(current_provider, next_provider, reason, context)
|
86
|
+
Aidp.logger.info("provider_manager", "Provider switched successfully", from: current_provider, to: next_provider, reason: reason)
|
82
87
|
return next_provider
|
88
|
+
else
|
89
|
+
Aidp.logger.warn("provider_manager", "Failed to switch to provider", provider: next_provider, reason: reason)
|
83
90
|
end
|
84
91
|
end
|
85
92
|
|
@@ -107,14 +114,21 @@ module Aidp
|
|
107
114
|
|
108
115
|
# No providers available
|
109
116
|
log_no_providers_available(reason, context)
|
117
|
+
Aidp.logger.error("provider_manager", "No providers available for fallback", reason: reason, **context)
|
110
118
|
nil
|
111
119
|
end
|
112
120
|
|
113
121
|
# Switch provider for specific error type
|
114
122
|
def switch_provider_for_error(error_type, error_details = {})
|
123
|
+
Aidp.logger.warn("provider_manager", "Error triggered provider switch", error_type: error_type, **error_details)
|
124
|
+
|
115
125
|
case error_type
|
116
126
|
when "rate_limit"
|
117
127
|
switch_provider("rate_limit", error_details)
|
128
|
+
when "resource_exhausted", "quota_exceeded"
|
129
|
+
# Treat capacity/resource exhaustion like rate limit for fallback purposes
|
130
|
+
Aidp.logger.warn("provider_manager", "Resource/quota exhaustion detected", classified_from: error_type)
|
131
|
+
switch_provider("rate_limit", error_details.merge(classified_from: error_type))
|
118
132
|
when "authentication"
|
119
133
|
switch_provider("authentication_error", error_details)
|
120
134
|
when "network"
|
@@ -269,9 +283,27 @@ module Aidp
|
|
269
283
|
|
270
284
|
# Set current provider with enhanced validation
|
271
285
|
def set_current_provider(provider_name, reason = "manual_switch", context = {})
|
272
|
-
|
273
|
-
|
274
|
-
|
286
|
+
# Use provider_config for ConfigManager, provider_configured? for legacy Configuration
|
287
|
+
config_exists = if @configuration.respond_to?(:provider_config)
|
288
|
+
@configuration.provider_config(provider_name)
|
289
|
+
else
|
290
|
+
@configuration.provider_configured?(provider_name)
|
291
|
+
end
|
292
|
+
|
293
|
+
unless config_exists
|
294
|
+
Aidp.logger.warn("provider_manager", "Provider not configured", provider: provider_name)
|
295
|
+
return false
|
296
|
+
end
|
297
|
+
|
298
|
+
unless is_provider_healthy?(provider_name)
|
299
|
+
Aidp.logger.warn("provider_manager", "Provider not healthy", provider: provider_name)
|
300
|
+
return false
|
301
|
+
end
|
302
|
+
|
303
|
+
if is_provider_circuit_breaker_open?(provider_name)
|
304
|
+
Aidp.logger.warn("provider_manager", "Provider circuit breaker open", provider: provider_name)
|
305
|
+
return false
|
306
|
+
end
|
275
307
|
|
276
308
|
# Update provider health
|
277
309
|
update_provider_health(provider_name, "switched_to")
|
@@ -292,6 +324,7 @@ module Aidp
|
|
292
324
|
@current_model = default_model(provider_name)
|
293
325
|
|
294
326
|
@current_provider = provider_name
|
327
|
+
Aidp.logger.info("provider_manager", "Provider activated", provider: provider_name, reason: reason)
|
295
328
|
true
|
296
329
|
end
|
297
330
|
|
@@ -468,11 +501,24 @@ module Aidp
|
|
468
501
|
# Build default fallback chain
|
469
502
|
def build_default_fallback_chain(provider_name)
|
470
503
|
all_providers = configured_providers
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
504
|
+
|
505
|
+
# Harness-defined explicit ordering has priority
|
506
|
+
harness_fallbacks = if @configuration.respond_to?(:fallback_providers)
|
507
|
+
Array(@configuration.fallback_providers).map(&:to_s)
|
508
|
+
else
|
509
|
+
[]
|
510
|
+
end
|
511
|
+
|
512
|
+
# Construct ordered chain:
|
513
|
+
# 1. current provider first
|
514
|
+
# 2. harness fallback providers (excluding current and de-duplicated)
|
515
|
+
# 3. any remaining configured providers not already listed
|
516
|
+
ordered = [provider_name]
|
517
|
+
ordered += harness_fallbacks.reject { |p| p == provider_name || ordered.include?(p) }
|
518
|
+
ordered += all_providers.reject { |p| ordered.include?(p) }
|
519
|
+
|
520
|
+
@fallback_chains[provider_name] = ordered
|
521
|
+
ordered
|
476
522
|
end
|
477
523
|
|
478
524
|
# Find next healthy provider in fallback chain
|
@@ -1008,7 +1054,8 @@ module Aidp
|
|
1008
1054
|
require_relative "../providers/cursor"
|
1009
1055
|
installed = Aidp::Providers::Cursor.available?
|
1010
1056
|
end
|
1011
|
-
rescue LoadError
|
1057
|
+
rescue LoadError => e
|
1058
|
+
log_rescue(e, component: "provider_manager", action: "check_provider_availability", fallback: false, provider: provider_name)
|
1012
1059
|
installed = false
|
1013
1060
|
end
|
1014
1061
|
@unavailable_cache[provider_name] = installed
|
@@ -1048,7 +1095,8 @@ module Aidp
|
|
1048
1095
|
end
|
1049
1096
|
path = begin
|
1050
1097
|
Aidp::Util.which(binary)
|
1051
|
-
rescue
|
1098
|
+
rescue => e
|
1099
|
+
log_rescue(e, component: "provider_manager", action: "locate_binary", fallback: nil, binary: binary)
|
1052
1100
|
nil
|
1053
1101
|
end
|
1054
1102
|
unless path
|
@@ -1074,13 +1122,15 @@ module Aidp
|
|
1074
1122
|
# Timeout -> kill
|
1075
1123
|
begin
|
1076
1124
|
Process.kill("TERM", pid)
|
1077
|
-
rescue
|
1125
|
+
rescue => e
|
1126
|
+
log_rescue(e, component: "provider_manager", action: "kill_timeout_process_term", fallback: nil, binary: binary, pid: pid)
|
1078
1127
|
nil
|
1079
1128
|
end
|
1080
1129
|
sleep 0.1
|
1081
1130
|
begin
|
1082
1131
|
Process.kill("KILL", pid)
|
1083
|
-
rescue
|
1132
|
+
rescue => e
|
1133
|
+
log_rescue(e, component: "provider_manager", action: "kill_timeout_process_kill", fallback: nil, binary: binary, pid: pid)
|
1084
1134
|
nil
|
1085
1135
|
end
|
1086
1136
|
ok = false
|
@@ -1093,6 +1143,7 @@ module Aidp
|
|
1093
1143
|
ok = true
|
1094
1144
|
end
|
1095
1145
|
rescue => e
|
1146
|
+
log_rescue(e, component: "provider_manager", action: "verify_binary_health", fallback: "binary_error", binary: binary)
|
1096
1147
|
ok = false
|
1097
1148
|
reason = e.class.name.downcase.include?("enoent") ? "binary_missing" : "binary_error"
|
1098
1149
|
end
|
@@ -1340,6 +1391,7 @@ module Aidp
|
|
1340
1391
|
}
|
1341
1392
|
}
|
1342
1393
|
rescue => e
|
1394
|
+
log_rescue(e, component: "provider_manager", action: "execute_with_provider", fallback: "error_result", provider: provider_type, prompt_length: prompt.length)
|
1343
1395
|
# Return error result
|
1344
1396
|
{
|
1345
1397
|
status: "error",
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require "securerandom"
|
4
4
|
require "yaml"
|
5
5
|
require "fileutils"
|
6
|
+
require_relative "../rescue_logging"
|
6
7
|
|
7
8
|
module Aidp
|
8
9
|
module Jobs
|
@@ -10,6 +11,7 @@ module Aidp
|
|
10
11
|
# Runs harness in daemon process and tracks job metadata
|
11
12
|
class BackgroundRunner
|
12
13
|
include Aidp::MessageDisplay
|
14
|
+
include Aidp::RescueLogging
|
13
15
|
|
14
16
|
attr_reader :project_dir, :jobs_dir
|
15
17
|
|
@@ -55,6 +57,7 @@ module Aidp
|
|
55
57
|
puts "[#{Time.now}] Job completed with status: #{result[:status]}"
|
56
58
|
mark_job_completed(job_id, result)
|
57
59
|
rescue => e
|
60
|
+
log_rescue(e, component: "background_runner", action: "execute_job", fallback: "mark_failed", job_id: job_id, mode: mode)
|
58
61
|
puts "[#{Time.now}] Job failed with error: #{e.message}"
|
59
62
|
puts e.backtrace.join("\n")
|
60
63
|
mark_job_failed(job_id, e)
|
@@ -136,11 +139,13 @@ module Aidp
|
|
136
139
|
|
137
140
|
mark_job_stopped(job_id)
|
138
141
|
{success: true, message: "Job stopped successfully"}
|
139
|
-
rescue Errno::ESRCH
|
142
|
+
rescue Errno::ESRCH => e
|
143
|
+
log_rescue(e, component: "background_runner", action: "stop_job", fallback: "mark_stopped", job_id: job_id, pid: pid, level: :info)
|
140
144
|
# Process already dead
|
141
145
|
mark_job_stopped(job_id)
|
142
146
|
{success: true, message: "Job was already stopped"}
|
143
147
|
rescue => e
|
148
|
+
log_rescue(e, component: "background_runner", action: "stop_job", fallback: "error_result", job_id: job_id, pid: pid)
|
144
149
|
{success: false, message: "Failed to stop job: #{e.message}"}
|
145
150
|
end
|
146
151
|
end
|
@@ -244,7 +249,8 @@ module Aidp
|
|
244
249
|
|
245
250
|
Process.kill(0, pid)
|
246
251
|
true
|
247
|
-
rescue Errno::ESRCH, Errno::EPERM
|
252
|
+
rescue Errno::ESRCH, Errno::EPERM => e
|
253
|
+
log_rescue(e, component: "background_runner", action: "check_process_running", fallback: false, pid: pid, level: :debug)
|
248
254
|
false
|
249
255
|
end
|
250
256
|
|
@@ -252,7 +258,8 @@ module Aidp
|
|
252
258
|
# Try to load checkpoint from project directory
|
253
259
|
checkpoint = Aidp::Execute::Checkpoint.new(@project_dir)
|
254
260
|
checkpoint.latest_checkpoint
|
255
|
-
rescue
|
261
|
+
rescue => e
|
262
|
+
log_rescue(e, component: "background_runner", action: "get_job_checkpoint", fallback: nil, job_id: job_id)
|
256
263
|
nil
|
257
264
|
end
|
258
265
|
|
data/lib/aidp/logger.rb
CHANGED
@@ -16,7 +16,7 @@ module Aidp
|
|
16
16
|
# Usage:
|
17
17
|
# Aidp.setup_logger(project_dir, config)
|
18
18
|
# Aidp.logger.info("component", "message", key: "value")
|
19
|
-
class
|
19
|
+
class Logger
|
20
20
|
LEVELS = {
|
21
21
|
debug: ::Logger::DEBUG,
|
22
22
|
info: ::Logger::INFO,
|
@@ -26,7 +26,6 @@ module Aidp
|
|
26
26
|
|
27
27
|
LOG_DIR = ".aidp/logs"
|
28
28
|
INFO_LOG = "#{LOG_DIR}/aidp.log"
|
29
|
-
DEBUG_LOG = "#{LOG_DIR}/aidp_debug.log"
|
30
29
|
|
31
30
|
DEFAULT_MAX_SIZE = 10 * 1024 * 1024 # 10MB
|
32
31
|
DEFAULT_MAX_FILES = 5
|
@@ -42,8 +41,7 @@ module Aidp
|
|
42
41
|
@max_files = config[:max_backups] || DEFAULT_MAX_FILES
|
43
42
|
|
44
43
|
ensure_log_directory
|
45
|
-
|
46
|
-
setup_loggers
|
44
|
+
setup_logger
|
47
45
|
end
|
48
46
|
|
49
47
|
# Log info level message
|
@@ -74,23 +72,12 @@ module Aidp
|
|
74
72
|
safe_message = redact(message)
|
75
73
|
safe_metadata = redact_hash(metadata)
|
76
74
|
|
77
|
-
|
78
|
-
if level == :debug
|
79
|
-
write_to_debug(level, component, safe_message, safe_metadata)
|
80
|
-
else
|
81
|
-
write_to_info(level, component, safe_message, safe_metadata)
|
82
|
-
end
|
83
|
-
|
84
|
-
# Always log errors to both files
|
85
|
-
if level == :error
|
86
|
-
write_to_debug(level, component, safe_message, safe_metadata)
|
87
|
-
end
|
75
|
+
write_entry(level, component, safe_message, safe_metadata)
|
88
76
|
end
|
89
77
|
|
90
78
|
# Close all loggers
|
91
79
|
def close
|
92
|
-
@
|
93
|
-
@debug_logger&.close
|
80
|
+
@logger&.close
|
94
81
|
end
|
95
82
|
|
96
83
|
private
|
@@ -111,12 +98,9 @@ module Aidp
|
|
111
98
|
FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
|
112
99
|
end
|
113
100
|
|
114
|
-
def
|
101
|
+
def setup_logger
|
115
102
|
info_path = File.join(@project_dir, INFO_LOG)
|
116
|
-
|
117
|
-
|
118
|
-
@info_logger = create_logger(info_path)
|
119
|
-
@debug_logger = create_logger(debug_path)
|
103
|
+
@logger = create_logger(info_path)
|
120
104
|
end
|
121
105
|
|
122
106
|
def create_logger(path)
|
@@ -126,14 +110,9 @@ module Aidp
|
|
126
110
|
logger
|
127
111
|
end
|
128
112
|
|
129
|
-
def
|
113
|
+
def write_entry(level, component, message, metadata)
|
130
114
|
entry = format_entry(level, component, message, metadata)
|
131
|
-
@
|
132
|
-
end
|
133
|
-
|
134
|
-
def write_to_debug(level, component, message, metadata)
|
135
|
-
entry = format_entry(level, component, message, metadata)
|
136
|
-
@debug_logger.send(logger_method(level), entry)
|
115
|
+
@logger.send(logger_method(level), entry)
|
137
116
|
end
|
138
117
|
|
139
118
|
def logger_method(level)
|
@@ -205,58 +184,18 @@ module Aidp
|
|
205
184
|
def redact_hash(hash)
|
206
185
|
hash.transform_values { |v| v.is_a?(String) ? redact(v) : v }
|
207
186
|
end
|
208
|
-
|
209
|
-
# Migration from old debug_logs location
|
210
|
-
OLD_DEBUG_DIR = ".aidp/debug_logs"
|
211
|
-
OLD_DEBUG_LOG = "#{OLD_DEBUG_DIR}/aidp_debug.log"
|
212
|
-
|
213
|
-
def should_migrate?
|
214
|
-
old_path = File.join(@project_dir, OLD_DEBUG_LOG)
|
215
|
-
new_path = File.join(@project_dir, DEBUG_LOG)
|
216
|
-
|
217
|
-
# Migrate if old exists and new doesn't
|
218
|
-
File.exist?(old_path) && !File.exist?(new_path)
|
219
|
-
end
|
220
|
-
|
221
|
-
def migrate_old_logs
|
222
|
-
old_path = File.join(@project_dir, OLD_DEBUG_LOG)
|
223
|
-
new_path = File.join(@project_dir, DEBUG_LOG)
|
224
|
-
|
225
|
-
begin
|
226
|
-
FileUtils.mv(old_path, new_path)
|
227
|
-
log_migration_notice
|
228
|
-
rescue => e
|
229
|
-
# If migration fails, just continue (new logs will be created)
|
230
|
-
warn "Failed to migrate old logs: #{e.message}"
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
def log_migration_notice
|
235
|
-
notice = format_text(
|
236
|
-
:info,
|
237
|
-
"migration",
|
238
|
-
"Logs migrated from .aidp/debug_logs/ to .aidp/logs/",
|
239
|
-
timestamp: Time.now.utc.iso8601
|
240
|
-
)
|
241
|
-
|
242
|
-
# Write directly to avoid recursion
|
243
|
-
info_path = File.join(@project_dir, INFO_LOG)
|
244
|
-
File.open(info_path, "a") do |f|
|
245
|
-
f.puts notice
|
246
|
-
end
|
247
|
-
end
|
248
187
|
end
|
249
188
|
|
250
189
|
# Module-level logger accessor
|
251
190
|
class << self
|
252
191
|
# Set up global logger instance
|
253
192
|
def setup_logger(project_dir = Dir.pwd, config = {})
|
254
|
-
@logger =
|
193
|
+
@logger = Logger.new(project_dir, config)
|
255
194
|
end
|
256
195
|
|
257
196
|
# Get current logger instance (creates default if not set up)
|
258
197
|
def logger
|
259
|
-
@logger ||=
|
198
|
+
@logger ||= Logger.new
|
260
199
|
end
|
261
200
|
|
262
201
|
# Convenience logging methods
|
data/lib/aidp/providers/base.rb
CHANGED
@@ -86,6 +86,16 @@ module Aidp
|
|
86
86
|
# Log the results
|
87
87
|
debug_command("copilot", args: args, input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
|
88
88
|
|
89
|
+
# Detect authorization/access errors
|
90
|
+
auth_error = result.err.to_s =~ /not authorized|requires an enterprise|access denied|permission denied|not enabled/i
|
91
|
+
if auth_error
|
92
|
+
spinner.error("ā")
|
93
|
+
mark_failed("copilot authorization error: #{result.err}")
|
94
|
+
@unavailable = true
|
95
|
+
debug_error(StandardError.new("copilot authorization error"), {exit_code: result.exit_status, stderr: result.err})
|
96
|
+
raise Aidp::Providers::ProviderUnavailableError.new("copilot authorization error: #{result.err}")
|
97
|
+
end
|
98
|
+
|
89
99
|
if result.exit_status == 0
|
90
100
|
spinner.success("ā")
|
91
101
|
mark_completed
|
@@ -96,6 +106,8 @@ module Aidp
|
|
96
106
|
debug_error(StandardError.new("copilot failed"), {exit_code: result.exit_status, stderr: result.err})
|
97
107
|
raise "copilot failed with exit code #{result.exit_status}: #{result.err}"
|
98
108
|
end
|
109
|
+
rescue Aidp::Providers::ProviderUnavailableError
|
110
|
+
raise
|
99
111
|
rescue => e
|
100
112
|
spinner&.error("ā")
|
101
113
|
mark_failed("copilot execution failed: #{e.message}")
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Aidp
|
5
|
+
# Mixin providing a unified helper for logging rescued exceptions.
|
6
|
+
# Usage:
|
7
|
+
# include Aidp::RescueLogging
|
8
|
+
# rescue => e
|
9
|
+
# log_rescue(e, component: "storage", action: "store file", fallback: {success: false})
|
10
|
+
#
|
11
|
+
# Defaults:
|
12
|
+
# - level: :warn (so filtering WARN surfaces rescue sites)
|
13
|
+
# - includes error class, message
|
14
|
+
# - optional fallback and extra context hash merged in
|
15
|
+
module RescueLogging
|
16
|
+
def log_rescue(error, component:, action:, fallback: nil, level: :warn, **context)
|
17
|
+
data = {
|
18
|
+
error_class: error.class.name,
|
19
|
+
error_message: error.message,
|
20
|
+
action: action
|
21
|
+
}
|
22
|
+
data[:fallback] = fallback if fallback
|
23
|
+
data.merge!(context) unless context.empty?
|
24
|
+
|
25
|
+
# Prefer debug_mixin if present; otherwise use Aidp.logger directly
|
26
|
+
if respond_to?(:debug_log)
|
27
|
+
debug_log("ā ļø Rescue in #{component}: #{action}", level: level, data: data)
|
28
|
+
else
|
29
|
+
Aidp.logger.send(level, component, "Rescued exception during #{action}", **data)
|
30
|
+
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
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/aidp/setup/wizard.rb
CHANGED
@@ -143,46 +143,35 @@ module Aidp
|
|
143
143
|
# Save primary provider
|
144
144
|
set([:harness, :default_provider], provider_choice) unless provider_choice == "custom"
|
145
145
|
|
146
|
-
|
146
|
+
ensure_provider_billing_config(provider_choice) unless provider_choice == "custom"
|
147
|
+
|
148
|
+
# Prompt for fallback providers (excluding the primary), pre-select existing
|
149
|
+
existing_fallbacks = Array(get([:harness, :fallback_providers])).map(&:to_s) - [provider_choice]
|
147
150
|
fallback_choices = available_providers.reject { |_, name| name == provider_choice }
|
148
|
-
fallback_selected = prompt.multi_select("Select fallback providers (used if primary fails):") do |menu|
|
151
|
+
fallback_selected = prompt.multi_select("Select fallback providers (used if primary fails):", default: existing_fallbacks) do |menu|
|
149
152
|
fallback_choices.each do |display_name, provider_name|
|
150
153
|
menu.choice display_name, provider_name
|
151
154
|
end
|
152
155
|
end
|
153
156
|
|
154
|
-
#
|
157
|
+
# If user selected none but we had existing fallbacks, confirm removal
|
158
|
+
if fallback_selected.empty? && existing_fallbacks.any?
|
159
|
+
keep = prompt.no?("No fallbacks selected. Remove existing fallbacks (#{existing_fallbacks.join(", ")})?", default: false)
|
160
|
+
fallback_selected = existing_fallbacks if keep
|
161
|
+
end
|
162
|
+
|
163
|
+
# Remove any accidental duplication of primary provider & save (preserve order)
|
155
164
|
cleaned_fallbacks = fallback_selected.reject { |name| name == provider_choice }
|
156
165
|
set([:harness, :fallback_providers], cleaned_fallbacks)
|
157
166
|
|
158
|
-
#
|
167
|
+
# Auto-create minimal provider configs for fallbacks if missing
|
168
|
+
cleaned_fallbacks.each { |fp| ensure_provider_billing_config(fp) }
|
159
169
|
|
160
|
-
|
161
|
-
|
170
|
+
# Provide informational note (no secret handling stored)
|
171
|
+
show_provider_info_note(provider_choice) unless provider_choice == "custom"
|
162
172
|
end
|
163
173
|
|
164
|
-
|
165
|
-
existing = get([:providers, :mcp]) || {}
|
166
|
-
enabled = prompt.yes?("Enable MCP (Model Context Protocol) tools?", default: existing.fetch(:enabled, true))
|
167
|
-
return delete_path([:providers, :mcp]) unless enabled
|
168
|
-
|
169
|
-
# TODO: Add default back once TTY-Prompt default validation issue is resolved
|
170
|
-
tools = prompt.multi_select("Select MCP tools:") do |menu|
|
171
|
-
menu.choice "Git", "git"
|
172
|
-
menu.choice "Shell", "shell"
|
173
|
-
menu.choice "Filesystem", "fs"
|
174
|
-
menu.choice "Browser", "browser"
|
175
|
-
menu.choice "GitHub", "github"
|
176
|
-
end
|
177
|
-
|
178
|
-
custom = ask_list("Custom MCP servers (comma-separated)", existing.fetch(:custom_servers, []))
|
179
|
-
|
180
|
-
set([:providers, :mcp], {
|
181
|
-
enabled: true,
|
182
|
-
tools: tools,
|
183
|
-
custom_servers: custom
|
184
|
-
}.compact)
|
185
|
-
end
|
174
|
+
# Removed MCP configuration step (MCP now expected to be provider-specific if used)
|
186
175
|
|
187
176
|
# -------------------------------------------
|
188
177
|
# Work loop configuration
|
@@ -621,27 +610,34 @@ module Aidp
|
|
621
610
|
:other
|
622
611
|
end
|
623
612
|
|
624
|
-
def
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
613
|
+
def show_provider_info_note(provider)
|
614
|
+
prompt.say("\nš” Provider integration:")
|
615
|
+
prompt.say("AIDP does not store API keys or model lists. Configure the agent (#{provider}) externally.")
|
616
|
+
prompt.say("Only the billing model (subscription vs usage_based) is recorded for fallback decisions.")
|
617
|
+
end
|
618
|
+
|
619
|
+
# Ensure a minimal billing configuration exists for a selected provider (no secrets)
|
620
|
+
def ensure_provider_billing_config(provider_name)
|
621
|
+
return if provider_name.nil? || provider_name == "custom"
|
622
|
+
providers_section = get([:providers]) || {}
|
623
|
+
existing = providers_section[provider_name.to_sym]
|
624
|
+
|
625
|
+
if existing && existing[:type]
|
626
|
+
prompt.say(" ⢠Provider '#{provider_name}' already configured (type: #{existing[:type]})")
|
627
|
+
return
|
631
628
|
end
|
629
|
+
|
630
|
+
provider_type = ask_provider_billing_type(provider_name)
|
631
|
+
set([:providers, provider_name.to_sym], {type: provider_type})
|
632
|
+
prompt.say(" ⢠Added provider '#{provider_name}' with billing type '#{provider_type}' (no secrets stored)")
|
632
633
|
end
|
633
634
|
|
634
|
-
def
|
635
|
-
prompt.
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
prompt.say("Export API key: export OPENAI_API_KEY=sk-...")
|
641
|
-
when "google"
|
642
|
-
prompt.say("Export API key: export GOOGLE_API_KEY=...")
|
643
|
-
else
|
644
|
-
prompt.say("Configure API credentials via environment variables.")
|
635
|
+
def ask_provider_billing_type(provider_name)
|
636
|
+
prompt.select("Billing model for #{provider_name}:") do |menu|
|
637
|
+
menu.choice "Subscription / flat-rate", "subscription"
|
638
|
+
# e.g. tools that expose an integrated model under a subscription cost
|
639
|
+
menu.choice "Usage-based / metered (API)", "usage_based"
|
640
|
+
menu.choice "Passthrough / local (no billing)", "passthrough"
|
645
641
|
end
|
646
642
|
end
|
647
643
|
|
@@ -2,11 +2,14 @@
|
|
2
2
|
|
3
3
|
require "csv"
|
4
4
|
require "fileutils"
|
5
|
+
require "aidp/rescue_logging"
|
5
6
|
|
6
7
|
module Aidp
|
7
8
|
module Storage
|
8
9
|
# Simple CSV file storage for tabular data
|
9
10
|
class CsvStorage
|
11
|
+
include Aidp::RescueLogging
|
12
|
+
|
10
13
|
def initialize(base_dir = ".aidp")
|
11
14
|
@base_dir = base_dir
|
12
15
|
ensure_directory_exists
|
@@ -42,11 +45,13 @@ module Aidp
|
|
42
45
|
success: true
|
43
46
|
}
|
44
47
|
rescue => error
|
45
|
-
|
48
|
+
log_rescue(error,
|
49
|
+
component: "csv_storage",
|
50
|
+
action: "append",
|
51
|
+
fallback: {success: false},
|
46
52
|
filename: filename,
|
47
|
-
|
48
|
-
|
49
|
-
}
|
53
|
+
path: file_path)
|
54
|
+
{filename: filename, error: error.message, success: false}
|
50
55
|
end
|
51
56
|
|
52
57
|
# Read all rows from CSV file
|
@@ -60,7 +65,12 @@ module Aidp
|
|
60
65
|
end
|
61
66
|
rows
|
62
67
|
rescue => error
|
63
|
-
|
68
|
+
log_rescue(error,
|
69
|
+
component: "csv_storage",
|
70
|
+
action: "read_all",
|
71
|
+
fallback: [],
|
72
|
+
filename: filename,
|
73
|
+
path: (defined?(file_path) ? file_path : nil))
|
64
74
|
[]
|
65
75
|
end
|
66
76
|
|
@@ -83,7 +93,12 @@ module Aidp
|
|
83
93
|
CSV.foreach(file_path) { count += 1 }
|
84
94
|
count - 1 # Subtract 1 for header row
|
85
95
|
rescue => error
|
86
|
-
|
96
|
+
log_rescue(error,
|
97
|
+
component: "csv_storage",
|
98
|
+
action: "count_rows",
|
99
|
+
fallback: 0,
|
100
|
+
filename: filename,
|
101
|
+
path: (defined?(file_path) ? file_path : nil))
|
87
102
|
0
|
88
103
|
end
|
89
104
|
|
@@ -127,7 +142,12 @@ module Aidp
|
|
127
142
|
|
128
143
|
summary_data
|
129
144
|
rescue => error
|
130
|
-
|
145
|
+
log_rescue(error,
|
146
|
+
component: "csv_storage",
|
147
|
+
action: "summary",
|
148
|
+
fallback: nil,
|
149
|
+
filename: filename,
|
150
|
+
path: (defined?(file_path) ? file_path : nil))
|
131
151
|
nil
|
132
152
|
end
|
133
153
|
|
@@ -144,6 +164,12 @@ module Aidp
|
|
144
164
|
File.delete(file_path)
|
145
165
|
{success: true, message: "File deleted"}
|
146
166
|
rescue => error
|
167
|
+
log_rescue(error,
|
168
|
+
component: "csv_storage",
|
169
|
+
action: "delete",
|
170
|
+
fallback: {success: false},
|
171
|
+
filename: filename,
|
172
|
+
path: file_path)
|
147
173
|
{success: false, error: error.message}
|
148
174
|
end
|
149
175
|
|