aidp 0.23.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aidp/cli.rb +3 -0
  3. data/lib/aidp/execute/work_loop_runner.rb +252 -45
  4. data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
  5. data/lib/aidp/harness/condition_detector.rb +42 -8
  6. data/lib/aidp/harness/config_manager.rb +7 -0
  7. data/lib/aidp/harness/config_schema.rb +25 -0
  8. data/lib/aidp/harness/configuration.rb +69 -6
  9. data/lib/aidp/harness/error_handler.rb +117 -44
  10. data/lib/aidp/harness/provider_manager.rb +64 -0
  11. data/lib/aidp/harness/provider_metrics.rb +138 -0
  12. data/lib/aidp/harness/runner.rb +90 -29
  13. data/lib/aidp/harness/simple_user_interface.rb +4 -0
  14. data/lib/aidp/harness/state/ui_state.rb +0 -10
  15. data/lib/aidp/harness/state_manager.rb +1 -15
  16. data/lib/aidp/harness/test_runner.rb +39 -2
  17. data/lib/aidp/logger.rb +34 -4
  18. data/lib/aidp/providers/adapter.rb +241 -0
  19. data/lib/aidp/providers/anthropic.rb +75 -7
  20. data/lib/aidp/providers/base.rb +29 -1
  21. data/lib/aidp/providers/capability_registry.rb +205 -0
  22. data/lib/aidp/providers/codex.rb +14 -0
  23. data/lib/aidp/providers/error_taxonomy.rb +195 -0
  24. data/lib/aidp/providers/gemini.rb +3 -2
  25. data/lib/aidp/setup/provider_registry.rb +107 -0
  26. data/lib/aidp/setup/wizard.rb +115 -31
  27. data/lib/aidp/version.rb +1 -1
  28. data/lib/aidp/watch/build_processor.rb +263 -23
  29. data/lib/aidp/watch/repository_client.rb +4 -4
  30. data/lib/aidp/watch/runner.rb +37 -5
  31. data/lib/aidp/workflows/guided_agent.rb +53 -0
  32. data/lib/aidp/worktree.rb +67 -10
  33. data/templates/work_loop/decide_whats_next.md +21 -0
  34. data/templates/work_loop/diagnose_failures.md +21 -0
  35. metadata +10 -3
  36. /data/{bin → exe}/aidp +0 -0
@@ -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
@@ -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
- edit_provider_configuration(to_edit)
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
@@ -1115,22 +1120,81 @@ module Aidp
1115
1120
  end
1116
1121
 
1117
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)
1118
1124
  prompt.say("\n📋 Provider Configuration Summary:")
1119
1125
  providers_config = get([:providers]) || {}
1120
1126
 
1121
- # Show primary
1127
+ rows = []
1128
+
1129
+ # Add primary provider to table
1122
1130
  if primary && primary != "custom"
1123
1131
  primary_cfg = providers_config[primary.to_sym] || {}
1124
- prompt.say(" ✓ Primary: #{primary} (#{primary_cfg[:type] || "not configured"}, #{primary_cfg[:model_family] || "auto"})")
1132
+ rows << [
1133
+ "Primary",
1134
+ primary,
1135
+ primary_cfg[:type] || "not configured",
1136
+ primary_cfg[:model_family] || "auto"
1137
+ ]
1125
1138
  end
1126
1139
 
1127
- # Show fallbacks
1140
+ # Add fallback providers to table
1128
1141
  if fallbacks && !fallbacks.empty?
1129
- fallbacks.each do |fallback|
1142
+ fallbacks.each_with_index do |fallback, index|
1130
1143
  fallback_cfg = providers_config[fallback.to_sym] || {}
1131
- prompt.say(" ✓ Fallback: #{fallback} (#{fallback_cfg[:type] || "not configured"}, #{fallback_cfg[:model_family] || "auto"})")
1144
+ rows << [
1145
+ "Fallback #{index + 1}",
1146
+ fallback,
1147
+ fallback_cfg[:type] || "not configured",
1148
+ fallback_cfg[:model_family] || "auto"
1149
+ ]
1132
1150
  end
1133
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
1134
1198
  end
1135
1199
 
1136
1200
  # Ensure a minimal billing configuration exists for a selected provider (no secrets)
@@ -1163,6 +1227,42 @@ module Aidp
1163
1227
  prompt.say(" • #{action_word.capitalize} provider '#{display_name}' (#{provider_name}) with billing type '#{provider_type}' and model family '#{model_family}'")
1164
1228
  end
1165
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
+
1166
1266
  def edit_provider_configuration(provider_name)
1167
1267
  existing = get([:providers, provider_name.to_sym]) || {}
1168
1268
  prompt.say("\n🔧 Editing provider '#{provider_name}' (current: type=#{existing[:type] || "unset"}, model_family=#{existing[:model_family] || "unset"})")
@@ -1178,54 +1278,38 @@ module Aidp
1178
1278
  ask_provider_billing_type_with_default(provider_name, nil)
1179
1279
  end
1180
1280
 
1181
- BILLING_TYPE_CHOICES = [
1182
- ["Subscription / flat-rate", "subscription"],
1183
- ["Usage-based / metered (API)", "usage_based"],
1184
- ["Passthrough / local (no billing)", "passthrough"]
1185
- ].freeze
1186
-
1187
1281
  def ask_provider_billing_type_with_default(provider_name, default_value)
1188
- default_label = BILLING_TYPE_CHOICES.find { |label, value| value == default_value }&.first
1282
+ choices = ProviderRegistry.billing_type_choices
1283
+ default_label = choices.find { |label, value| value == default_value }&.first
1189
1284
  suffix = default_value ? " (current: #{default_value})" : ""
1190
1285
  prompt.select("Billing model for #{provider_name}:#{suffix}", default: default_label) do |menu|
1191
- BILLING_TYPE_CHOICES.each do |label, value|
1286
+ choices.each do |label, value|
1192
1287
  menu.choice(label, value)
1193
1288
  end
1194
1289
  end
1195
1290
  end
1196
1291
 
1197
- MODEL_FAMILY_CHOICES = [
1198
- ["Auto (let provider decide)", "auto"],
1199
- ["OpenAI o-series (reasoning models)", "openai_o"],
1200
- ["Anthropic Claude (balanced)", "claude"],
1201
- ["Mistral (European/open)", "mistral"],
1202
- ["Local LLM (self-hosted)", "local"]
1203
- ].freeze
1204
-
1205
1292
  def ask_model_family(provider_name, default = "auto")
1206
1293
  # TTY::Prompt validates defaults against the displayed choice labels, not values.
1207
1294
  # Map the value default (e.g. "auto") to its corresponding label.
1208
- default_label = MODEL_FAMILY_CHOICES.find { |label, value| value == default }&.first
1295
+ choices = ProviderRegistry.model_family_choices
1296
+ default_label = choices.find { |label, value| value == default }&.first
1209
1297
 
1210
1298
  prompt.select("Preferred model family for #{provider_name}:", default: default_label) do |menu|
1211
- MODEL_FAMILY_CHOICES.each do |label, value|
1299
+ choices.each do |label, value|
1212
1300
  menu.choice(label, value)
1213
1301
  end
1214
1302
  end
1215
1303
  end
1216
1304
 
1217
1305
  # Canonicalization helpers ------------------------------------------------
1218
- MODEL_FAMILY_LABEL_TO_VALUE = MODEL_FAMILY_CHOICES.each_with_object({}) do |(label, value), h|
1219
- h[label] = value
1220
- end.freeze
1221
- MODEL_FAMILY_VALUES = MODEL_FAMILY_CHOICES.map { |(_, value)| value }.freeze
1222
-
1223
1306
  def normalize_model_family(value)
1224
1307
  return "auto" if value.nil? || value.to_s.strip.empty?
1225
1308
  # Already a canonical value
1226
- return value if MODEL_FAMILY_VALUES.include?(value)
1309
+ return value if ProviderRegistry.valid_model_family?(value)
1227
1310
  # Try label -> value
1228
- mapped = MODEL_FAMILY_LABEL_TO_VALUE[value]
1311
+ choices = ProviderRegistry.model_family_choices
1312
+ mapped = choices.find { |label, _| label == value }&.last
1229
1313
  return mapped if mapped
1230
1314
  # Unknown legacy entry -> fallback to auto
1231
1315
  "auto"
data/lib/aidp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.23.0"
4
+ VERSION = "0.24.0"
5
5
  end