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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module Providers
|
|
5
|
+
# ErrorTaxonomy defines the five standardized error categories that all providers
|
|
6
|
+
# use for consistent error handling, retry logic, and escalation.
|
|
7
|
+
#
|
|
8
|
+
# Categories:
|
|
9
|
+
# - rate_limited: Provider is rate-limiting requests (switch provider immediately)
|
|
10
|
+
# - auth_expired: Authentication credentials are invalid or expired (escalate or switch)
|
|
11
|
+
# - quota_exceeded: Usage quota has been exceeded (switch provider)
|
|
12
|
+
# - transient: Temporary error that may resolve on retry (retry with backoff)
|
|
13
|
+
# - permanent: Permanent error that won't resolve with retry (escalate or abort)
|
|
14
|
+
#
|
|
15
|
+
# @see https://github.com/viamin/aidp/issues/243
|
|
16
|
+
module ErrorTaxonomy
|
|
17
|
+
# Error category constants
|
|
18
|
+
RATE_LIMITED = :rate_limited
|
|
19
|
+
AUTH_EXPIRED = :auth_expired
|
|
20
|
+
QUOTA_EXCEEDED = :quota_exceeded
|
|
21
|
+
TRANSIENT = :transient
|
|
22
|
+
PERMANENT = :permanent
|
|
23
|
+
|
|
24
|
+
# All valid error categories
|
|
25
|
+
CATEGORIES = [
|
|
26
|
+
RATE_LIMITED,
|
|
27
|
+
AUTH_EXPIRED,
|
|
28
|
+
QUOTA_EXCEEDED,
|
|
29
|
+
TRANSIENT,
|
|
30
|
+
PERMANENT
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
# Default error patterns for common error messages
|
|
34
|
+
# Providers can override these with provider-specific patterns
|
|
35
|
+
DEFAULT_PATTERNS = {
|
|
36
|
+
rate_limited: [
|
|
37
|
+
/rate.?limit/i,
|
|
38
|
+
/too.?many.?requests/i,
|
|
39
|
+
/429/,
|
|
40
|
+
/throttl(ed|ing)/i,
|
|
41
|
+
/request.?limit/i,
|
|
42
|
+
/requests.?per.?minute/i,
|
|
43
|
+
/rpm.?exceeded/i
|
|
44
|
+
],
|
|
45
|
+
auth_expired: [
|
|
46
|
+
/auth(entication|orization).?(fail(ed|ure)|error)/i,
|
|
47
|
+
/invalid.?(api.?key|token|credential)/i,
|
|
48
|
+
/expired.?(api.?key|token|credential)/i,
|
|
49
|
+
/unauthorized/i,
|
|
50
|
+
/401/,
|
|
51
|
+
/403/,
|
|
52
|
+
/permission.?denied/i,
|
|
53
|
+
/access.?denied/i
|
|
54
|
+
],
|
|
55
|
+
quota_exceeded: [
|
|
56
|
+
/quota.?(exceed(ed)?|limit|exhausted)/i,
|
|
57
|
+
/usage.?limit/i,
|
|
58
|
+
/billing.?limit/i,
|
|
59
|
+
/credit.?limit/i,
|
|
60
|
+
/insufficient.?quota/i,
|
|
61
|
+
/usage.?cap/i
|
|
62
|
+
],
|
|
63
|
+
transient: [
|
|
64
|
+
/timeout/i,
|
|
65
|
+
/timed?.?out/i,
|
|
66
|
+
/connection.?(reset|refused|lost|closed)/i,
|
|
67
|
+
/temporary.?error/i,
|
|
68
|
+
/try.?again/i,
|
|
69
|
+
/service.?unavailable/i,
|
|
70
|
+
/503/,
|
|
71
|
+
/502/,
|
|
72
|
+
/504/,
|
|
73
|
+
/gateway.?timeout/i,
|
|
74
|
+
/network.?error/i,
|
|
75
|
+
/socket.?error/i,
|
|
76
|
+
/connection.?error/i,
|
|
77
|
+
/broken.?pipe/i,
|
|
78
|
+
/host.?unreachable/i
|
|
79
|
+
],
|
|
80
|
+
permanent: [
|
|
81
|
+
/invalid.?(model|parameter|request|input)/i,
|
|
82
|
+
/unsupported.?(operation|feature|model)/i,
|
|
83
|
+
/not.?found/i,
|
|
84
|
+
/404/,
|
|
85
|
+
/bad.?request/i,
|
|
86
|
+
/400/,
|
|
87
|
+
/malformed/i,
|
|
88
|
+
/syntax.?error/i,
|
|
89
|
+
/validation.?error/i,
|
|
90
|
+
/model.?not.?available/i,
|
|
91
|
+
/model.?deprecated/i
|
|
92
|
+
]
|
|
93
|
+
}.freeze
|
|
94
|
+
|
|
95
|
+
# Retry policy for each category
|
|
96
|
+
RETRY_POLICIES = {
|
|
97
|
+
rate_limited: {
|
|
98
|
+
retry: false,
|
|
99
|
+
switch_provider: true,
|
|
100
|
+
escalate: false,
|
|
101
|
+
backoff_strategy: :none
|
|
102
|
+
},
|
|
103
|
+
auth_expired: {
|
|
104
|
+
retry: false,
|
|
105
|
+
switch_provider: true,
|
|
106
|
+
escalate: true,
|
|
107
|
+
backoff_strategy: :none
|
|
108
|
+
},
|
|
109
|
+
quota_exceeded: {
|
|
110
|
+
retry: false,
|
|
111
|
+
switch_provider: true,
|
|
112
|
+
escalate: false,
|
|
113
|
+
backoff_strategy: :none
|
|
114
|
+
},
|
|
115
|
+
transient: {
|
|
116
|
+
retry: true,
|
|
117
|
+
switch_provider: false,
|
|
118
|
+
escalate: false,
|
|
119
|
+
backoff_strategy: :exponential
|
|
120
|
+
},
|
|
121
|
+
permanent: {
|
|
122
|
+
retry: false,
|
|
123
|
+
switch_provider: false,
|
|
124
|
+
escalate: true,
|
|
125
|
+
backoff_strategy: :none
|
|
126
|
+
}
|
|
127
|
+
}.freeze
|
|
128
|
+
|
|
129
|
+
# Check if a category is valid
|
|
130
|
+
# @param category [Symbol] category to check
|
|
131
|
+
# @return [Boolean] true if valid
|
|
132
|
+
def self.valid_category?(category)
|
|
133
|
+
CATEGORIES.include?(category)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get retry policy for a category
|
|
137
|
+
# @param category [Symbol] error category
|
|
138
|
+
# @return [Hash] retry policy configuration
|
|
139
|
+
def self.retry_policy(category)
|
|
140
|
+
RETRY_POLICIES[category] || RETRY_POLICIES[:transient]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Classify an error message using default patterns
|
|
144
|
+
# @param message [String] error message
|
|
145
|
+
# @return [Symbol] error category
|
|
146
|
+
def self.classify_message(message)
|
|
147
|
+
return :transient if message.nil? || message.empty?
|
|
148
|
+
|
|
149
|
+
message_lower = message.downcase
|
|
150
|
+
|
|
151
|
+
# Check each category's patterns
|
|
152
|
+
DEFAULT_PATTERNS.each do |category, patterns|
|
|
153
|
+
patterns.each do |pattern|
|
|
154
|
+
return category if message_lower.match?(pattern)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Default to transient for unknown errors
|
|
159
|
+
:transient
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Check if an error category is retryable
|
|
163
|
+
# @param category [Symbol] error category
|
|
164
|
+
# @return [Boolean] true if should retry
|
|
165
|
+
def self.retryable?(category)
|
|
166
|
+
policy = retry_policy(category)
|
|
167
|
+
policy[:retry] == true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Check if an error category should trigger provider switch
|
|
171
|
+
# @param category [Symbol] error category
|
|
172
|
+
# @return [Boolean] true if should switch provider
|
|
173
|
+
def self.should_switch_provider?(category)
|
|
174
|
+
policy = retry_policy(category)
|
|
175
|
+
policy[:switch_provider] == true
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Check if an error category should be escalated
|
|
179
|
+
# @param category [Symbol] error category
|
|
180
|
+
# @return [Boolean] true if should escalate
|
|
181
|
+
def self.should_escalate?(category)
|
|
182
|
+
policy = retry_policy(category)
|
|
183
|
+
policy[:escalate] == true
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Get backoff strategy for a category
|
|
187
|
+
# @param category [Symbol] error category
|
|
188
|
+
# @return [Symbol] backoff strategy (:none, :linear, :exponential)
|
|
189
|
+
def self.backoff_strategy(category)
|
|
190
|
+
policy = retry_policy(category)
|
|
191
|
+
policy[:backoff_strategy] || :none
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -36,11 +36,12 @@ module Aidp
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
begin
|
|
39
|
+
command_args = ["--prompt", prompt]
|
|
39
40
|
# Use debug_execute_command with streaming support
|
|
40
|
-
result = debug_execute_command("gemini", args:
|
|
41
|
+
result = debug_execute_command("gemini", args: command_args, timeout: timeout_seconds, streaming: streaming_enabled)
|
|
41
42
|
|
|
42
43
|
# Log the results
|
|
43
|
-
debug_command("gemini", args:
|
|
44
|
+
debug_command("gemini", args: command_args, input: nil, output: result.out, error: result.err, exit_code: result.exit_status)
|
|
44
45
|
|
|
45
46
|
if result.exit_status == 0
|
|
46
47
|
result.out
|
|
@@ -10,9 +10,10 @@ module Aidp
|
|
|
10
10
|
class BackupManager
|
|
11
11
|
class BackupError < StandardError; end
|
|
12
12
|
|
|
13
|
-
def initialize(project_dir)
|
|
13
|
+
def initialize(project_dir, clock: Time)
|
|
14
14
|
@project_dir = project_dir
|
|
15
15
|
@backup_dir = File.join(project_dir, ".aidp", "backups", "devcontainer")
|
|
16
|
+
@clock = clock
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
# Create a backup of the devcontainer file
|
|
@@ -26,7 +27,7 @@ module Aidp
|
|
|
26
27
|
|
|
27
28
|
ensure_backup_directory_exists
|
|
28
29
|
|
|
29
|
-
timestamp =
|
|
30
|
+
timestamp = current_time.utc.strftime("%Y%m%d_%H%M%S")
|
|
30
31
|
backup_filename = "devcontainer-#{timestamp}.json"
|
|
31
32
|
backup_path = File.join(@backup_dir, backup_filename)
|
|
32
33
|
|
|
@@ -163,11 +164,17 @@ module Aidp
|
|
|
163
164
|
end
|
|
164
165
|
|
|
165
166
|
def parse_timestamp(timestamp_str)
|
|
166
|
-
return
|
|
167
|
+
return current_time if timestamp_str.nil?
|
|
167
168
|
|
|
168
169
|
Time.strptime(timestamp_str, "%Y%m%d_%H%M%S")
|
|
169
170
|
rescue ArgumentError
|
|
170
|
-
|
|
171
|
+
current_time
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
attr_reader :clock
|
|
175
|
+
|
|
176
|
+
def current_time
|
|
177
|
+
clock.respond_to?(:call) ? clock.call : clock.now
|
|
171
178
|
end
|
|
172
179
|
end
|
|
173
180
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module Setup
|
|
5
|
+
# Centralized registry for provider metadata including billing types and model families.
|
|
6
|
+
# This module provides a single source of truth for provider configuration options.
|
|
7
|
+
module ProviderRegistry
|
|
8
|
+
# Billing type options for providers
|
|
9
|
+
BILLING_TYPES = [
|
|
10
|
+
{
|
|
11
|
+
label: "Subscription / flat-rate",
|
|
12
|
+
value: "subscription",
|
|
13
|
+
description: "Monthly or annual subscription with unlimited usage"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: "Usage-based / metered (API)",
|
|
17
|
+
value: "usage_based",
|
|
18
|
+
description: "Pay per API call or token usage"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
label: "Passthrough / local (no billing)",
|
|
22
|
+
value: "passthrough",
|
|
23
|
+
description: "Local execution or proxy without direct billing"
|
|
24
|
+
}
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
# Model family options for providers
|
|
28
|
+
MODEL_FAMILIES = [
|
|
29
|
+
{
|
|
30
|
+
label: "Auto (let provider decide)",
|
|
31
|
+
value: "auto",
|
|
32
|
+
description: "Use provider's default model selection"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: "OpenAI o-series (reasoning models)",
|
|
36
|
+
value: "openai_o",
|
|
37
|
+
description: "Advanced reasoning capabilities, slower but more thorough"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: "Anthropic Claude (balanced)",
|
|
41
|
+
value: "claude",
|
|
42
|
+
description: "Balanced performance for general-purpose tasks"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: "Mistral (European/open)",
|
|
46
|
+
value: "mistral",
|
|
47
|
+
description: "European provider with open-source focus"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: "Local LLM (self-hosted)",
|
|
51
|
+
value: "local",
|
|
52
|
+
description: "Self-hosted or local model execution"
|
|
53
|
+
}
|
|
54
|
+
].freeze
|
|
55
|
+
|
|
56
|
+
# Returns array of [label, value] pairs for billing types
|
|
57
|
+
def self.billing_type_choices
|
|
58
|
+
BILLING_TYPES.map { |bt| [bt[:label], bt[:value]] }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns array of [label, value] pairs for model families
|
|
62
|
+
def self.model_family_choices
|
|
63
|
+
MODEL_FAMILIES.map { |mf| [mf[:label], mf[:value]] }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Finds label for a given billing type value
|
|
67
|
+
def self.billing_type_label(value)
|
|
68
|
+
BILLING_TYPES.find { |bt| bt[:value] == value }&.dig(:label) || value
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Finds label for a given model family value
|
|
72
|
+
def self.model_family_label(value)
|
|
73
|
+
MODEL_FAMILIES.find { |mf| mf[:value] == value }&.dig(:label) || value
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Finds description for a given billing type value
|
|
77
|
+
def self.billing_type_description(value)
|
|
78
|
+
BILLING_TYPES.find { |bt| bt[:value] == value }&.dig(:description)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Finds description for a given model family value
|
|
82
|
+
def self.model_family_description(value)
|
|
83
|
+
MODEL_FAMILIES.find { |mf| mf[:value] == value }&.dig(:description)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Validates if a billing type value is valid
|
|
87
|
+
def self.valid_billing_type?(value)
|
|
88
|
+
BILLING_TYPES.any? { |bt| bt[:value] == value }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Validates if a model family value is valid
|
|
92
|
+
def self.valid_model_family?(value)
|
|
93
|
+
MODEL_FAMILIES.any? { |mf| mf[:value] == value }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns all valid billing type values
|
|
97
|
+
def self.billing_type_values
|
|
98
|
+
BILLING_TYPES.map { |bt| bt[:value] }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Returns all valid model family values
|
|
102
|
+
def self.model_family_values
|
|
103
|
+
MODEL_FAMILIES.map { |mf| mf[:value] }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/aidp/setup/wizard.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "tty-prompt"
|
|
4
|
+
require "tty-table"
|
|
4
5
|
require "yaml"
|
|
5
6
|
require "time"
|
|
6
7
|
require "fileutils"
|
|
@@ -8,6 +9,7 @@ require "json"
|
|
|
8
9
|
|
|
9
10
|
require_relative "../util"
|
|
10
11
|
require_relative "../config/paths"
|
|
12
|
+
require_relative "provider_registry"
|
|
11
13
|
require_relative "devcontainer/parser"
|
|
12
14
|
require_relative "devcontainer/generator"
|
|
13
15
|
require_relative "devcontainer/port_manager"
|
|
@@ -260,7 +262,10 @@ module Aidp
|
|
|
260
262
|
editable = ([provider_choice] + cleaned_fallbacks).uniq.reject { |p| p == "custom" }
|
|
261
263
|
end
|
|
262
264
|
else
|
|
263
|
-
|
|
265
|
+
# Edit the selected provider or offer to remove it
|
|
266
|
+
edit_or_remove_provider(to_edit, provider_choice, cleaned_fallbacks)
|
|
267
|
+
# Refresh editable list after potential removal
|
|
268
|
+
editable = ([provider_choice] + cleaned_fallbacks).uniq.reject { |p| p == "custom" }
|
|
264
269
|
end
|
|
265
270
|
end
|
|
266
271
|
end
|
|
@@ -786,6 +791,79 @@ module Aidp
|
|
|
786
791
|
watch_enabled: watch,
|
|
787
792
|
quick_mode_default: quick_mode
|
|
788
793
|
})
|
|
794
|
+
|
|
795
|
+
# Configure watch mode settings if enabled
|
|
796
|
+
configure_watch_mode if watch
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
def configure_watch_mode
|
|
800
|
+
prompt.say("\n👀 Watch Mode Configuration")
|
|
801
|
+
prompt.say("-" * 40)
|
|
802
|
+
|
|
803
|
+
configure_watch_safety
|
|
804
|
+
configure_watch_labels
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def configure_watch_safety
|
|
808
|
+
prompt.say("\n🔒 Watch mode safety settings")
|
|
809
|
+
existing = get([:watch, :safety]) || {}
|
|
810
|
+
|
|
811
|
+
allow_public_repos = prompt.yes?(
|
|
812
|
+
"Allow watch mode on public repositories?",
|
|
813
|
+
default: existing.fetch(:allow_public_repos, false)
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
prompt.say("\n📝 Author allowlist (GitHub usernames allowed to trigger watch mode)")
|
|
817
|
+
prompt.say(" Leave empty to allow all authors (not recommended for public repos)")
|
|
818
|
+
author_allowlist = ask_list(
|
|
819
|
+
"Author allowlist (comma-separated GitHub usernames)",
|
|
820
|
+
existing[:author_allowlist] || [],
|
|
821
|
+
allow_empty: true
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
require_container = prompt.yes?(
|
|
825
|
+
"Require watch mode to run in a container?",
|
|
826
|
+
default: existing.fetch(:require_container, true)
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
set([:watch, :safety], {
|
|
830
|
+
allow_public_repos: allow_public_repos,
|
|
831
|
+
author_allowlist: author_allowlist,
|
|
832
|
+
require_container: require_container
|
|
833
|
+
})
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def configure_watch_labels
|
|
837
|
+
prompt.say("\n🏷️ Watch mode label configuration")
|
|
838
|
+
prompt.say(" Configure GitHub issue labels that trigger watch mode actions")
|
|
839
|
+
existing = get([:watch, :labels]) || {}
|
|
840
|
+
|
|
841
|
+
plan_trigger = ask_with_default(
|
|
842
|
+
"Label to trigger plan generation",
|
|
843
|
+
existing[:plan_trigger] || "aidp-plan"
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
needs_input = ask_with_default(
|
|
847
|
+
"Label for plans needing user input",
|
|
848
|
+
existing[:needs_input] || "aidp-needs-input"
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
ready_to_build = ask_with_default(
|
|
852
|
+
"Label for plans ready to build",
|
|
853
|
+
existing[:ready_to_build] || "aidp-ready"
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
build_trigger = ask_with_default(
|
|
857
|
+
"Label to trigger implementation",
|
|
858
|
+
existing[:build_trigger] || "aidp-build"
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
set([:watch, :labels], {
|
|
862
|
+
plan_trigger: plan_trigger,
|
|
863
|
+
needs_input: needs_input,
|
|
864
|
+
ready_to_build: ready_to_build,
|
|
865
|
+
build_trigger: build_trigger
|
|
866
|
+
})
|
|
789
867
|
end
|
|
790
868
|
|
|
791
869
|
# -------------------------------------------
|
|
@@ -816,6 +894,7 @@ module Aidp
|
|
|
816
894
|
.sub(/^nfrs:/, "# Non-functional requirements to reference during planning\nnfrs:")
|
|
817
895
|
.sub(/^logging:/, "# Logging configuration\nlogging:")
|
|
818
896
|
.sub(/^modes:/, "# Defaults for background/watch/quick modes\nmodes:")
|
|
897
|
+
.sub(/^watch:/, "# Watch mode safety and label configuration\nwatch:")
|
|
819
898
|
end
|
|
820
899
|
|
|
821
900
|
def display_preview(yaml_content)
|
|
@@ -1041,22 +1120,81 @@ module Aidp
|
|
|
1041
1120
|
end
|
|
1042
1121
|
|
|
1043
1122
|
def show_provider_summary(primary, fallbacks)
|
|
1123
|
+
Aidp.log_debug("wizard.provider_summary", "displaying provider configuration table", primary: primary, fallback_count: fallbacks&.size || 0)
|
|
1044
1124
|
prompt.say("\n📋 Provider Configuration Summary:")
|
|
1045
1125
|
providers_config = get([:providers]) || {}
|
|
1046
1126
|
|
|
1047
|
-
|
|
1127
|
+
rows = []
|
|
1128
|
+
|
|
1129
|
+
# Add primary provider to table
|
|
1048
1130
|
if primary && primary != "custom"
|
|
1049
1131
|
primary_cfg = providers_config[primary.to_sym] || {}
|
|
1050
|
-
|
|
1132
|
+
rows << [
|
|
1133
|
+
"Primary",
|
|
1134
|
+
primary,
|
|
1135
|
+
primary_cfg[:type] || "not configured",
|
|
1136
|
+
primary_cfg[:model_family] || "auto"
|
|
1137
|
+
]
|
|
1051
1138
|
end
|
|
1052
1139
|
|
|
1053
|
-
#
|
|
1140
|
+
# Add fallback providers to table
|
|
1054
1141
|
if fallbacks && !fallbacks.empty?
|
|
1055
|
-
fallbacks.
|
|
1142
|
+
fallbacks.each_with_index do |fallback, index|
|
|
1056
1143
|
fallback_cfg = providers_config[fallback.to_sym] || {}
|
|
1057
|
-
|
|
1144
|
+
rows << [
|
|
1145
|
+
"Fallback #{index + 1}",
|
|
1146
|
+
fallback,
|
|
1147
|
+
fallback_cfg[:type] || "not configured",
|
|
1148
|
+
fallback_cfg[:model_family] || "auto"
|
|
1149
|
+
]
|
|
1058
1150
|
end
|
|
1059
1151
|
end
|
|
1152
|
+
|
|
1153
|
+
# Detect duplicate providers with identical characteristics
|
|
1154
|
+
duplicates = detect_duplicate_providers(rows)
|
|
1155
|
+
if duplicates.any?
|
|
1156
|
+
Aidp.log_warn("wizard.provider_summary", "duplicate provider configurations detected", duplicates: duplicates)
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
if rows.any?
|
|
1160
|
+
table = TTY::Table.new(
|
|
1161
|
+
header: ["Role", "Provider", "Billing Type", "Model Family"],
|
|
1162
|
+
rows: rows
|
|
1163
|
+
)
|
|
1164
|
+
prompt.say(table.render(:unicode, padding: [0, 1]))
|
|
1165
|
+
|
|
1166
|
+
# Show warning for duplicates
|
|
1167
|
+
if duplicates.any?
|
|
1168
|
+
prompt.say("")
|
|
1169
|
+
prompt.warn("⚠️ Duplicate configurations detected:")
|
|
1170
|
+
duplicates.each do |dup|
|
|
1171
|
+
prompt.say(" • #{dup[:providers].join(" and ")} have identical billing type (#{dup[:type]}) and model family (#{dup[:family]})")
|
|
1172
|
+
end
|
|
1173
|
+
prompt.say(" Consider using different providers or model families for better redundancy.")
|
|
1174
|
+
end
|
|
1175
|
+
else
|
|
1176
|
+
prompt.say(" (No providers configured)")
|
|
1177
|
+
end
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
def detect_duplicate_providers(rows)
|
|
1181
|
+
# Group providers by their billing type and model family
|
|
1182
|
+
# Returns array of duplicate groups with identical characteristics
|
|
1183
|
+
duplicates = []
|
|
1184
|
+
config_groups = rows.group_by { |row| [row[2], row[3]] }
|
|
1185
|
+
|
|
1186
|
+
config_groups.each do |(type, family), group|
|
|
1187
|
+
next if group.size < 2
|
|
1188
|
+
next if type == "not configured" # Skip unconfigured providers
|
|
1189
|
+
|
|
1190
|
+
duplicates << {
|
|
1191
|
+
providers: group.map { |row| row[1] },
|
|
1192
|
+
type: type,
|
|
1193
|
+
family: family
|
|
1194
|
+
}
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
duplicates
|
|
1060
1198
|
end
|
|
1061
1199
|
|
|
1062
1200
|
# Ensure a minimal billing configuration exists for a selected provider (no secrets)
|
|
@@ -1089,6 +1227,42 @@ module Aidp
|
|
|
1089
1227
|
prompt.say(" • #{action_word.capitalize} provider '#{display_name}' (#{provider_name}) with billing type '#{provider_type}' and model family '#{model_family}'")
|
|
1090
1228
|
end
|
|
1091
1229
|
|
|
1230
|
+
def edit_or_remove_provider(provider_name, primary_provider, fallbacks)
|
|
1231
|
+
is_primary = (provider_name == primary_provider)
|
|
1232
|
+
display_name = discover_available_providers.invert.fetch(provider_name, provider_name)
|
|
1233
|
+
|
|
1234
|
+
action = prompt.select("What would you like to do with '#{display_name}'?") do |menu|
|
|
1235
|
+
menu.choice "Edit configuration", :edit
|
|
1236
|
+
unless is_primary
|
|
1237
|
+
menu.choice "Remove from configuration", :remove
|
|
1238
|
+
end
|
|
1239
|
+
menu.choice "Cancel", :cancel
|
|
1240
|
+
end
|
|
1241
|
+
|
|
1242
|
+
case action
|
|
1243
|
+
when :edit
|
|
1244
|
+
edit_provider_configuration(provider_name)
|
|
1245
|
+
when :remove
|
|
1246
|
+
if is_primary
|
|
1247
|
+
prompt.warn("Cannot remove primary provider. Change primary provider first.")
|
|
1248
|
+
else
|
|
1249
|
+
remove_fallback_provider(provider_name, fallbacks)
|
|
1250
|
+
end
|
|
1251
|
+
when :cancel
|
|
1252
|
+
Aidp.log_debug("wizard.edit_provider", "user cancelled edit operation", provider: provider_name)
|
|
1253
|
+
end
|
|
1254
|
+
end
|
|
1255
|
+
|
|
1256
|
+
def remove_fallback_provider(provider_name, fallbacks)
|
|
1257
|
+
display_name = discover_available_providers.invert.fetch(provider_name, provider_name)
|
|
1258
|
+
if prompt.yes?("Remove '#{display_name}' from fallback providers?", default: false)
|
|
1259
|
+
fallbacks.delete(provider_name)
|
|
1260
|
+
set([:harness, :fallback_providers], fallbacks)
|
|
1261
|
+
Aidp.log_info("wizard.remove_provider", "removed fallback provider", provider: provider_name)
|
|
1262
|
+
prompt.ok("Removed '#{display_name}' from fallback providers")
|
|
1263
|
+
end
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1092
1266
|
def edit_provider_configuration(provider_name)
|
|
1093
1267
|
existing = get([:providers, provider_name.to_sym]) || {}
|
|
1094
1268
|
prompt.say("\n🔧 Editing provider '#{provider_name}' (current: type=#{existing[:type] || "unset"}, model_family=#{existing[:model_family] || "unset"})")
|
|
@@ -1104,54 +1278,38 @@ module Aidp
|
|
|
1104
1278
|
ask_provider_billing_type_with_default(provider_name, nil)
|
|
1105
1279
|
end
|
|
1106
1280
|
|
|
1107
|
-
BILLING_TYPE_CHOICES = [
|
|
1108
|
-
["Subscription / flat-rate", "subscription"],
|
|
1109
|
-
["Usage-based / metered (API)", "usage_based"],
|
|
1110
|
-
["Passthrough / local (no billing)", "passthrough"]
|
|
1111
|
-
].freeze
|
|
1112
|
-
|
|
1113
1281
|
def ask_provider_billing_type_with_default(provider_name, default_value)
|
|
1114
|
-
|
|
1282
|
+
choices = ProviderRegistry.billing_type_choices
|
|
1283
|
+
default_label = choices.find { |label, value| value == default_value }&.first
|
|
1115
1284
|
suffix = default_value ? " (current: #{default_value})" : ""
|
|
1116
1285
|
prompt.select("Billing model for #{provider_name}:#{suffix}", default: default_label) do |menu|
|
|
1117
|
-
|
|
1286
|
+
choices.each do |label, value|
|
|
1118
1287
|
menu.choice(label, value)
|
|
1119
1288
|
end
|
|
1120
1289
|
end
|
|
1121
1290
|
end
|
|
1122
1291
|
|
|
1123
|
-
MODEL_FAMILY_CHOICES = [
|
|
1124
|
-
["Auto (let provider decide)", "auto"],
|
|
1125
|
-
["OpenAI o-series (reasoning models)", "openai_o"],
|
|
1126
|
-
["Anthropic Claude (balanced)", "claude"],
|
|
1127
|
-
["Mistral (European/open)", "mistral"],
|
|
1128
|
-
["Local LLM (self-hosted)", "local"]
|
|
1129
|
-
].freeze
|
|
1130
|
-
|
|
1131
1292
|
def ask_model_family(provider_name, default = "auto")
|
|
1132
1293
|
# TTY::Prompt validates defaults against the displayed choice labels, not values.
|
|
1133
1294
|
# Map the value default (e.g. "auto") to its corresponding label.
|
|
1134
|
-
|
|
1295
|
+
choices = ProviderRegistry.model_family_choices
|
|
1296
|
+
default_label = choices.find { |label, value| value == default }&.first
|
|
1135
1297
|
|
|
1136
1298
|
prompt.select("Preferred model family for #{provider_name}:", default: default_label) do |menu|
|
|
1137
|
-
|
|
1299
|
+
choices.each do |label, value|
|
|
1138
1300
|
menu.choice(label, value)
|
|
1139
1301
|
end
|
|
1140
1302
|
end
|
|
1141
1303
|
end
|
|
1142
1304
|
|
|
1143
1305
|
# Canonicalization helpers ------------------------------------------------
|
|
1144
|
-
MODEL_FAMILY_LABEL_TO_VALUE = MODEL_FAMILY_CHOICES.each_with_object({}) do |(label, value), h|
|
|
1145
|
-
h[label] = value
|
|
1146
|
-
end.freeze
|
|
1147
|
-
MODEL_FAMILY_VALUES = MODEL_FAMILY_CHOICES.map { |(_, value)| value }.freeze
|
|
1148
|
-
|
|
1149
1306
|
def normalize_model_family(value)
|
|
1150
1307
|
return "auto" if value.nil? || value.to_s.strip.empty?
|
|
1151
1308
|
# Already a canonical value
|
|
1152
|
-
return value if
|
|
1309
|
+
return value if ProviderRegistry.valid_model_family?(value)
|
|
1153
1310
|
# Try label -> value
|
|
1154
|
-
|
|
1311
|
+
choices = ProviderRegistry.model_family_choices
|
|
1312
|
+
mapped = choices.find { |label, _| label == value }&.last
|
|
1155
1313
|
return mapped if mapped
|
|
1156
1314
|
# Unknown legacy entry -> fallback to auto
|
|
1157
1315
|
"auto"
|
data/lib/aidp/version.rb
CHANGED