aidp 0.23.0 → 0.25.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -1
  3. data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
  4. data/lib/aidp/auto_update/checkpoint.rb +178 -0
  5. data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
  6. data/lib/aidp/auto_update/coordinator.rb +204 -0
  7. data/lib/aidp/auto_update/errors.rb +17 -0
  8. data/lib/aidp/auto_update/failure_tracker.rb +162 -0
  9. data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
  10. data/lib/aidp/auto_update/update_check.rb +106 -0
  11. data/lib/aidp/auto_update/update_logger.rb +143 -0
  12. data/lib/aidp/auto_update/update_policy.rb +109 -0
  13. data/lib/aidp/auto_update/version_detector.rb +144 -0
  14. data/lib/aidp/auto_update.rb +52 -0
  15. data/lib/aidp/cli.rb +168 -1
  16. data/lib/aidp/execute/work_loop_runner.rb +252 -45
  17. data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
  18. data/lib/aidp/harness/condition_detector.rb +42 -8
  19. data/lib/aidp/harness/config_manager.rb +7 -0
  20. data/lib/aidp/harness/config_schema.rb +75 -0
  21. data/lib/aidp/harness/configuration.rb +69 -6
  22. data/lib/aidp/harness/error_handler.rb +117 -44
  23. data/lib/aidp/harness/provider_factory.rb +2 -0
  24. data/lib/aidp/harness/provider_manager.rb +64 -0
  25. data/lib/aidp/harness/provider_metrics.rb +138 -0
  26. data/lib/aidp/harness/runner.rb +90 -29
  27. data/lib/aidp/harness/simple_user_interface.rb +4 -0
  28. data/lib/aidp/harness/state/ui_state.rb +0 -10
  29. data/lib/aidp/harness/state_manager.rb +1 -15
  30. data/lib/aidp/harness/test_runner.rb +39 -2
  31. data/lib/aidp/logger.rb +34 -4
  32. data/lib/aidp/message_display.rb +10 -2
  33. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
  34. data/lib/aidp/provider_manager.rb +2 -0
  35. data/lib/aidp/providers/adapter.rb +241 -0
  36. data/lib/aidp/providers/anthropic.rb +75 -7
  37. data/lib/aidp/providers/base.rb +29 -1
  38. data/lib/aidp/providers/capability_registry.rb +205 -0
  39. data/lib/aidp/providers/codex.rb +14 -0
  40. data/lib/aidp/providers/error_taxonomy.rb +195 -0
  41. data/lib/aidp/providers/gemini.rb +3 -2
  42. data/lib/aidp/providers/kilocode.rb +202 -0
  43. data/lib/aidp/setup/provider_registry.rb +122 -0
  44. data/lib/aidp/setup/wizard.rb +125 -33
  45. data/lib/aidp/skills/composer.rb +4 -0
  46. data/lib/aidp/skills/loader.rb +3 -1
  47. data/lib/aidp/version.rb +1 -1
  48. data/lib/aidp/watch/build_processor.rb +323 -33
  49. data/lib/aidp/watch/ci_fix_processor.rb +448 -0
  50. data/lib/aidp/watch/plan_processor.rb +12 -2
  51. data/lib/aidp/watch/repository_client.rb +384 -4
  52. data/lib/aidp/watch/review_processor.rb +266 -0
  53. data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
  54. data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
  55. data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
  56. data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
  57. data/lib/aidp/watch/runner.rb +222 -5
  58. data/lib/aidp/watch/state_store.rb +53 -0
  59. data/lib/aidp/workflows/guided_agent.rb +53 -0
  60. data/lib/aidp/worktree.rb +67 -10
  61. data/lib/aidp.rb +1 -0
  62. data/templates/work_loop/decide_whats_next.md +21 -0
  63. data/templates/work_loop/diagnose_failures.md +21 -0
  64. metadata +29 -3
  65. /data/{bin → exe}/aidp +0 -0
@@ -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,24 +1120,83 @@ 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
+ ]
1150
+ end
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.")
1132
1174
  end
1175
+ else
1176
+ prompt.say(" (No providers configured)")
1133
1177
  end
1134
1178
  end
1135
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
1198
+ end
1199
+
1136
1200
  # Ensure a minimal billing configuration exists for a selected provider (no secrets)
1137
1201
  def ensure_provider_billing_config(provider_name, force: false)
1138
1202
  return if provider_name.nil? || provider_name == "custom"
@@ -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,55 +1278,47 @@ 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
- # Already a canonical value
1226
- return value if MODEL_FAMILY_VALUES.include?(value)
1227
- # Try label -> value
1228
- mapped = MODEL_FAMILY_LABEL_TO_VALUE[value]
1308
+
1309
+ normalized_input = value.to_s.strip.downcase
1310
+
1311
+ # Check for exact canonical value match (case-insensitive)
1312
+ canonical_match = ProviderRegistry.model_family_values.find do |v|
1313
+ v.downcase == normalized_input
1314
+ end
1315
+ return canonical_match if canonical_match
1316
+
1317
+ # Try label -> value mapping (case-insensitive)
1318
+ choices = ProviderRegistry.model_family_choices
1319
+ mapped = choices.find { |label, _| label.downcase == value.to_s.downcase }&.last
1229
1320
  return mapped if mapped
1321
+
1230
1322
  # Unknown legacy entry -> fallback to auto
1231
1323
  "auto"
1232
1324
  end
@@ -82,6 +82,8 @@ module Aidp
82
82
  def render_template(template, options: {})
83
83
  return template if options.empty?
84
84
 
85
+ # Ensure template is UTF-8 encoded
86
+ template = template.encode("UTF-8", invalid: :replace, undef: :replace) unless template.encoding == Encoding::UTF_8
85
87
  rendered = template.dup
86
88
 
87
89
  options.each do |key, value|
@@ -158,6 +160,8 @@ module Aidp
158
160
  def extract_placeholders(text)
159
161
  return [] if text.nil? || text.empty?
160
162
 
163
+ # Ensure text is UTF-8 encoded
164
+ text = text.encode("UTF-8", invalid: :replace, undef: :replace) unless text.encoding == Encoding::UTF_8
161
165
  scanner = StringScanner.new(text)
162
166
  placeholders = []
163
167
 
@@ -34,7 +34,7 @@ module Aidp
34
34
  raise Aidp::Errors::ValidationError, "Skill file not found: #{file_path}"
35
35
  end
36
36
 
37
- content = File.read(file_path)
37
+ content = File.read(file_path, encoding: "UTF-8")
38
38
  load_from_string(content, source_path: file_path, provider: provider)
39
39
  end
40
40
 
@@ -139,6 +139,8 @@ module Aidp
139
139
  # @return [Array(Hash, String)] Tuple of [metadata, markdown_content]
140
140
  # @raise [Aidp::Errors::ValidationError] if frontmatter is missing or invalid
141
141
  def self.parse_frontmatter(content, source_path:)
142
+ # Ensure content is UTF-8 encoded
143
+ content = content.encode("UTF-8", invalid: :replace, undef: :replace) unless content.encoding == Encoding::UTF_8
142
144
  lines = content.lines
143
145
 
144
146
  unless lines.first&.strip == "---"
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.25.0"
5
5
  end