aidp 0.25.0 → 0.27.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -6
  3. data/lib/aidp/analyze/error_handler.rb +11 -0
  4. data/lib/aidp/cli/checkpoint_command.rb +198 -0
  5. data/lib/aidp/cli/config_command.rb +71 -0
  6. data/lib/aidp/cli/enhanced_input.rb +2 -0
  7. data/lib/aidp/cli/first_run_wizard.rb +8 -7
  8. data/lib/aidp/cli/harness_command.rb +102 -0
  9. data/lib/aidp/cli/jobs_command.rb +3 -3
  10. data/lib/aidp/cli/mcp_dashboard.rb +4 -3
  11. data/lib/aidp/cli/models_command.rb +662 -0
  12. data/lib/aidp/cli/providers_command.rb +223 -0
  13. data/lib/aidp/cli.rb +35 -456
  14. data/lib/aidp/daemon/runner.rb +2 -2
  15. data/lib/aidp/debug_mixin.rb +2 -9
  16. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  17. data/lib/aidp/execute/checkpoint_display.rb +38 -37
  18. data/lib/aidp/execute/interactive_repl.rb +2 -1
  19. data/lib/aidp/execute/prompt_manager.rb +4 -4
  20. data/lib/aidp/execute/work_loop_runner.rb +253 -56
  21. data/lib/aidp/execute/workflow_selector.rb +2 -2
  22. data/lib/aidp/harness/config_loader.rb +20 -11
  23. data/lib/aidp/harness/config_manager.rb +5 -5
  24. data/lib/aidp/harness/config_schema.rb +30 -8
  25. data/lib/aidp/harness/configuration.rb +105 -4
  26. data/lib/aidp/harness/enhanced_runner.rb +24 -15
  27. data/lib/aidp/harness/error_handler.rb +26 -5
  28. data/lib/aidp/harness/filter_strategy.rb +45 -0
  29. data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
  30. data/lib/aidp/harness/model_cache.rb +269 -0
  31. data/lib/aidp/harness/model_discovery_service.rb +259 -0
  32. data/lib/aidp/harness/model_registry.rb +201 -0
  33. data/lib/aidp/harness/output_filter.rb +136 -0
  34. data/lib/aidp/harness/provider_manager.rb +18 -3
  35. data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
  36. data/lib/aidp/harness/runner.rb +5 -0
  37. data/lib/aidp/harness/test_runner.rb +165 -27
  38. data/lib/aidp/harness/thinking_depth_manager.rb +223 -7
  39. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
  40. data/lib/aidp/logger.rb +35 -5
  41. data/lib/aidp/providers/adapter.rb +2 -4
  42. data/lib/aidp/providers/anthropic.rb +141 -128
  43. data/lib/aidp/providers/base.rb +98 -2
  44. data/lib/aidp/providers/capability_registry.rb +0 -1
  45. data/lib/aidp/providers/codex.rb +49 -67
  46. data/lib/aidp/providers/cursor.rb +71 -59
  47. data/lib/aidp/providers/gemini.rb +44 -60
  48. data/lib/aidp/providers/github_copilot.rb +2 -66
  49. data/lib/aidp/providers/kilocode.rb +24 -80
  50. data/lib/aidp/providers/opencode.rb +24 -80
  51. data/lib/aidp/safe_directory.rb +10 -3
  52. data/lib/aidp/setup/wizard.rb +345 -8
  53. data/lib/aidp/storage/csv_storage.rb +9 -3
  54. data/lib/aidp/storage/file_manager.rb +8 -2
  55. data/lib/aidp/storage/json_storage.rb +9 -3
  56. data/lib/aidp/version.rb +1 -1
  57. data/lib/aidp/watch/build_processor.rb +40 -1
  58. data/lib/aidp/watch/change_request_processor.rb +659 -0
  59. data/lib/aidp/watch/plan_generator.rb +93 -14
  60. data/lib/aidp/watch/plan_processor.rb +71 -8
  61. data/lib/aidp/watch/repository_client.rb +85 -20
  62. data/lib/aidp/watch/review_processor.rb +3 -3
  63. data/lib/aidp/watch/runner.rb +37 -0
  64. data/lib/aidp/watch/state_store.rb +46 -1
  65. data/lib/aidp/workflows/guided_agent.rb +3 -3
  66. data/lib/aidp/workstream_executor.rb +5 -2
  67. data/lib/aidp.rb +4 -0
  68. data/templates/aidp-development.yml.example +2 -2
  69. data/templates/aidp-production.yml.example +3 -3
  70. data/templates/aidp.yml.example +53 -0
  71. metadata +14 -1
@@ -0,0 +1,662 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-table"
4
+ require "tty-prompt"
5
+ require "tty-spinner"
6
+ require_relative "../harness/model_registry"
7
+ require_relative "../harness/model_discovery_service"
8
+
9
+ module Aidp
10
+ class CLI
11
+ # Command handler for `aidp models` subcommand group
12
+ #
13
+ # Provides commands for viewing and discovering AI models:
14
+ # - list: Show all available models with tier information
15
+ # - discover: Discover models from configured providers
16
+ # - refresh: Refresh the model cache
17
+ #
18
+ # Usage:
19
+ # aidp models list [--provider=<name>] [--tier=<tier>]
20
+ # aidp models discover [--provider=<name>]
21
+ # aidp models refresh [--provider=<name>]
22
+ class ModelsCommand
23
+ include Aidp::MessageDisplay
24
+
25
+ def initialize(prompt: TTY::Prompt.new, registry: nil, discovery_service: nil)
26
+ @prompt = prompt
27
+ @registry = registry
28
+ @discovery_service = discovery_service
29
+ end
30
+
31
+ # Main entry point for models subcommand
32
+ def run(args)
33
+ subcommand = args.first if args.first && !args.first.start_with?("--")
34
+
35
+ case subcommand
36
+ when "list", nil
37
+ args.shift if subcommand == "list"
38
+ run_list_command(args)
39
+ when "discover"
40
+ args.shift
41
+ run_discover_command(args)
42
+ when "refresh"
43
+ args.shift
44
+ run_refresh_command(args)
45
+ when "validate"
46
+ args.shift
47
+ run_validate_command(args)
48
+ else
49
+ display_message("Unknown models subcommand: #{subcommand}", type: :error)
50
+ display_help
51
+ 1
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def registry
58
+ @registry ||= Aidp::Harness::ModelRegistry.new
59
+ end
60
+
61
+ def discovery_service
62
+ @discovery_service ||= Aidp::Harness::ModelDiscoveryService.new
63
+ end
64
+
65
+ def display_help
66
+ display_message("\nUsage: aidp models <subcommand> [options]", type: :info)
67
+ display_message("\nSubcommands:", type: :info)
68
+ display_message(" list List all available models with tier information", type: :info)
69
+ display_message(" discover Discover models from configured providers", type: :info)
70
+ display_message(" refresh Refresh the model discovery cache", type: :info)
71
+ display_message(" validate Validate model configuration for all tiers", type: :info)
72
+ display_message("\nOptions:", type: :info)
73
+ display_message(" --provider=<name> Filter/target specific provider", type: :info)
74
+ display_message(" --tier=<tier> Filter by tier (mini, standard, advanced)", type: :info)
75
+ display_message("\nExamples:", type: :info)
76
+ display_message(" aidp models list", type: :info)
77
+ display_message(" aidp models list --tier=mini", type: :info)
78
+ display_message(" aidp models list --provider=anthropic", type: :info)
79
+ display_message(" aidp models discover", type: :info)
80
+ display_message(" aidp models discover --provider=anthropic", type: :info)
81
+ display_message(" aidp models refresh", type: :info)
82
+ display_message(" aidp models validate", type: :info)
83
+ end
84
+
85
+ def run_list_command(args)
86
+ options = parse_list_options(args)
87
+
88
+ begin
89
+ # Get all model families from registry
90
+ families = registry.all_families
91
+
92
+ # Apply tier filter if specified
93
+ if options[:tier]
94
+ families = families.select { |family|
95
+ info = registry.get_model_info(family)
96
+ info && info["tier"] == options[:tier]
97
+ }
98
+ end
99
+
100
+ # Build table rows
101
+ rows = []
102
+ families.each do |family|
103
+ info = registry.get_model_info(family)
104
+ next unless info
105
+
106
+ # Get providers that support this family
107
+ providers = find_providers_for_family(family)
108
+
109
+ # Apply provider filter if specified
110
+ next if options[:provider] && !providers.include?(options[:provider])
111
+
112
+ # Add a row for each provider that supports this family
113
+ if providers.empty?
114
+ # No provider support - show as registry-only
115
+ rows << build_table_row(nil, family, info, "registry")
116
+ else
117
+ providers.each do |provider_name|
118
+ rows << build_table_row(provider_name, family, info, "registry")
119
+ end
120
+ end
121
+ end
122
+
123
+ # Sort rows by tier, then provider, then model name
124
+ tier_order = {"mini" => 0, "standard" => 1, "advanced" => 2}
125
+ rows.sort_by! { |r| [tier_order[r[2]] || 3, r[0] || "", r[1]] }
126
+
127
+ # Display table
128
+ if rows.empty?
129
+ display_message("No models found matching the specified criteria.", type: :info)
130
+ return 0
131
+ end
132
+
133
+ display_message("\n#{build_header(options)}\n", type: :highlight)
134
+
135
+ table = TTY::Table.new(
136
+ header: ["Provider", "Model Family", "Tier", "Capabilities", "Context", "Speed"],
137
+ rows: rows
138
+ )
139
+
140
+ # Use simple renderer for consistent formatting
141
+ renderer = table.render(:basic, padding: [0, 1])
142
+ display_message(renderer, type: :info)
143
+
144
+ display_message("\n#{build_footer(rows.size)}\n", type: :info)
145
+ 0
146
+ rescue Aidp::Harness::ModelRegistry::RegistryError => e
147
+ display_message("Error loading model registry: #{e.message}", type: :error)
148
+ Aidp.log_error("models_command", "registry error", error: e.message)
149
+ 1
150
+ rescue => e
151
+ display_message("Error listing models: #{e.message}", type: :error)
152
+ Aidp.log_error("models_command", "unexpected error", error: e.message, backtrace: e.backtrace.first(5))
153
+ 1
154
+ end
155
+ end
156
+
157
+ def parse_list_options(args)
158
+ options = {}
159
+
160
+ args.each do |arg|
161
+ case arg
162
+ when /^--provider=(.+)$/
163
+ options[:provider] = Regexp.last_match(1)
164
+ when /^--tier=(.+)$/
165
+ tier = Regexp.last_match(1)
166
+ unless Aidp::Harness::ModelRegistry::VALID_TIERS.include?(tier)
167
+ display_message("Invalid tier: #{tier}. Valid tiers: #{Aidp::Harness::ModelRegistry::VALID_TIERS.join(", ")}", type: :error)
168
+ exit 1
169
+ end
170
+ options[:tier] = tier
171
+ when "--help", "-h"
172
+ display_help
173
+ exit 0
174
+ end
175
+ end
176
+
177
+ options
178
+ end
179
+
180
+ def build_table_row(provider_name, family, info, source)
181
+ capabilities = (info["capabilities"] || []).join(",")
182
+ context = format_context_window(info["context_window"])
183
+ speed = info["speed"] || "unknown"
184
+
185
+ [
186
+ provider_name || "-",
187
+ family,
188
+ info["tier"] || "unknown",
189
+ capabilities.empty? ? "-" : capabilities,
190
+ context,
191
+ speed
192
+ ]
193
+ end
194
+
195
+ def format_context_window(tokens)
196
+ return "-" unless tokens
197
+
198
+ if tokens >= 1_000_000
199
+ "#{tokens / 1_000_000}M"
200
+ elsif tokens >= 1_000
201
+ "#{tokens / 1_000}K"
202
+ else
203
+ tokens.to_s
204
+ end
205
+ end
206
+
207
+ def build_header(options)
208
+ parts = ["Available Models"]
209
+ parts << "(Provider: #{options[:provider]})" if options[:provider]
210
+ parts << "(Tier: #{options[:tier]})" if options[:tier]
211
+ parts.join(" ")
212
+ end
213
+
214
+ def build_footer(count)
215
+ tips = [
216
+ "💡 Showing #{count} model#{"s" unless count == 1} from the static registry",
217
+ "💡 Model families are provider-agnostic (e.g., 'claude-3-5-sonnet' works across providers)"
218
+ ]
219
+ tips.join("\n")
220
+ end
221
+
222
+ def find_providers_for_family(family_name)
223
+ providers = []
224
+
225
+ # Check each provider adapter for support
226
+ provider_classes = [
227
+ Aidp::Providers::Anthropic,
228
+ Aidp::Providers::Cursor,
229
+ Aidp::Providers::Gemini
230
+ ]
231
+
232
+ provider_classes.each do |provider_class|
233
+ next unless provider_class.respond_to?(:supports_model_family?)
234
+
235
+ if provider_class.supports_model_family?(family_name)
236
+ # Get the provider name from an instance (need to instantiate to call name method)
237
+ # Or use a simple name mapping
238
+ provider_name = provider_class.name.split("::").last.downcase
239
+ providers << provider_name
240
+ end
241
+ rescue => e
242
+ # Log but don't fail if provider check fails
243
+ Aidp.log_debug("models_command", "provider check failed", provider: provider_class.name, error: e.message)
244
+ end
245
+
246
+ providers
247
+ end
248
+
249
+ def run_discover_command(args)
250
+ options = parse_discover_options(args)
251
+
252
+ begin
253
+ display_message("\n🔍 Discovering models from configured providers...\n", type: :highlight)
254
+
255
+ spinner = TTY::Spinner.new("[:spinner] Querying provider APIs...", format: :dots)
256
+ spinner.auto_spin
257
+
258
+ # Discover models
259
+ results = if options[:provider]
260
+ {options[:provider] => discovery_service.discover_models(options[:provider], use_cache: false)}
261
+ else
262
+ discovery_service.discover_all_models(use_cache: false)
263
+ end
264
+
265
+ spinner.success("✓")
266
+
267
+ # Display results
268
+ total_models = 0
269
+ results.each do |provider, models|
270
+ next if models.empty?
271
+
272
+ total_models += models.size
273
+ display_message("\n✓ Found #{models.size} models for #{provider}:", type: :success)
274
+
275
+ # Group by tier
276
+ by_tier = models.group_by { |m| m[:tier] }
277
+ %w[mini standard advanced].each do |tier|
278
+ tier_models = by_tier[tier] || []
279
+ next if tier_models.empty?
280
+
281
+ display_message(" #{tier.capitalize} tier:", type: :info)
282
+ tier_models.each do |model|
283
+ display_message(" - #{model[:name]}", type: :info)
284
+ end
285
+ end
286
+ end
287
+
288
+ if total_models == 0
289
+ display_message("\n⚠️ No models discovered. Ensure provider CLIs are installed and configured.", type: :warning)
290
+ return 1
291
+ end
292
+
293
+ display_message("\n✅ Discovered #{total_models} total model#{"s" unless total_models == 1}", type: :success)
294
+ display_message("💾 Models cached for 24 hours\n", type: :info)
295
+ 0
296
+ rescue => e
297
+ display_message("Error discovering models: #{e.message}", type: :error)
298
+ Aidp.log_error("models_command", "discovery error", error: e.message, backtrace: e.backtrace.first(5))
299
+ 1
300
+ end
301
+ end
302
+
303
+ def run_refresh_command(args)
304
+ options = parse_refresh_options(args)
305
+
306
+ begin
307
+ display_message("\n♻️ Refreshing model cache...\n", type: :highlight)
308
+
309
+ spinner = TTY::Spinner.new("[:spinner] Clearing cache and re-discovering...", format: :dots)
310
+ spinner.auto_spin
311
+
312
+ if options[:provider]
313
+ discovery_service.refresh_cache(options[:provider])
314
+ spinner.success("✓")
315
+ display_message("\n✅ Refreshed cache for #{options[:provider]}", type: :success)
316
+ else
317
+ discovery_service.refresh_all_caches
318
+ spinner.success("✓")
319
+ display_message("\n✅ Refreshed cache for all providers", type: :success)
320
+ end
321
+
322
+ display_message("💡 Run 'aidp models discover' to see the updated models\n", type: :info)
323
+ 0
324
+ rescue => e
325
+ display_message("Error refreshing cache: #{e.message}", type: :error)
326
+ Aidp.log_error("models_command", "refresh error", error: e.message, backtrace: e.backtrace.first(5))
327
+ 1
328
+ end
329
+ end
330
+
331
+ def parse_discover_options(args)
332
+ options = {}
333
+
334
+ args.each do |arg|
335
+ case arg
336
+ when /^--provider=(.+)$/
337
+ options[:provider] = Regexp.last_match(1)
338
+ when "--help", "-h"
339
+ display_help
340
+ exit 0
341
+ end
342
+ end
343
+
344
+ options
345
+ end
346
+
347
+ def parse_refresh_options(args)
348
+ parse_discover_options(args)
349
+ end
350
+
351
+ def run_validate_command(args)
352
+ parse_validate_options(args)
353
+
354
+ begin
355
+ # Load configuration
356
+ config = load_configuration
357
+ return 1 unless config
358
+
359
+ display_message("\n🔍 Validating model configuration...\n", type: :highlight)
360
+
361
+ # Collect validation issues
362
+ issues = []
363
+ warnings = []
364
+
365
+ # Validate tier coverage
366
+ tier_issues = validate_tier_coverage(config)
367
+ issues.concat(tier_issues[:errors])
368
+ warnings.concat(tier_issues[:warnings])
369
+
370
+ # Validate provider models
371
+ provider_issues = validate_provider_models(config)
372
+ issues.concat(provider_issues[:errors])
373
+ warnings.concat(provider_issues[:warnings])
374
+
375
+ # Display results
376
+ display_validation_results(issues, warnings)
377
+
378
+ # Return exit code
379
+ issues.empty? ? 0 : 1
380
+ rescue => e
381
+ display_message("Error validating configuration: #{e.message}", type: :error)
382
+ Aidp.log_error("models_command", "validation error",
383
+ error: e.message, backtrace: e.backtrace.first(5))
384
+ 1
385
+ end
386
+ end
387
+
388
+ def parse_validate_options(args)
389
+ args.each do |arg|
390
+ case arg
391
+ when "--help", "-h"
392
+ display_help
393
+ exit 0
394
+ end
395
+ end
396
+ end
397
+
398
+ def load_configuration
399
+ project_dir = Dir.pwd
400
+ unless Aidp::Config.config_exists?(project_dir)
401
+ display_message("❌ No aidp.yml configuration file found", type: :error)
402
+ display_message("Run 'aidp config --interactive' to create one", type: :info)
403
+ return nil
404
+ end
405
+
406
+ config_data = Aidp::Config.load(project_dir) || {}
407
+ providers_section = config_data[:providers] || config_data["providers"] || {}
408
+ SimpleConfiguration.new(providers_section)
409
+ rescue => e
410
+ display_message("Error validating configuration: #{e.message}", type: :error)
411
+ nil
412
+ end
413
+
414
+ def validate_tier_coverage(config)
415
+ errors = []
416
+ warnings = []
417
+
418
+ # Check each tier for model coverage
419
+ Aidp::Harness::ModelRegistry::VALID_TIERS.each do |tier|
420
+ has_model = tier_has_model?(config, tier)
421
+
422
+ unless has_model
423
+ errors << {
424
+ tier: tier,
425
+ message: "No model configured for '#{tier}' tier",
426
+ fix: generate_tier_fix_suggestion(tier, config)
427
+ }
428
+ end
429
+ end
430
+
431
+ {errors: errors, warnings: warnings}
432
+ end
433
+
434
+ def tier_has_model?(config, tier)
435
+ # Check if any provider has a model for this tier
436
+ configured_providers = config.configured_providers
437
+
438
+ configured_providers.any? do |provider_name|
439
+ provider_cfg = config.provider_config(provider_name)
440
+ tier_config = provider_cfg.dig(:thinking, :tiers, tier.to_sym) ||
441
+ provider_cfg.dig(:thinking, :tiers, tier)
442
+
443
+ tier_config && tier_config[:models] && !tier_config[:models].empty?
444
+ end
445
+ end
446
+
447
+ def validate_provider_models(config)
448
+ errors = []
449
+ warnings = []
450
+
451
+ configured_providers = config.configured_providers
452
+
453
+ configured_providers.each do |provider_name|
454
+ provider_class = get_provider_class(provider_name)
455
+ next unless provider_class
456
+
457
+ provider_cfg = config.provider_config(provider_name)
458
+ thinking_cfg = provider_cfg[:thinking] || {}
459
+ tiers_cfg = thinking_cfg[:tiers] || {}
460
+
461
+ # Validate models in each tier
462
+ tiers_cfg.each do |tier, tier_config|
463
+ next unless tier_config[:models]
464
+
465
+ tier_config[:models].each do |model_entry|
466
+ model_name = model_entry.is_a?(Hash) ? model_entry[:model] : model_entry
467
+ next unless model_name
468
+
469
+ # Check if provider supports this model family
470
+ family = get_model_family(provider_class, model_name)
471
+ next unless family
472
+
473
+ unless provider_supports_model?(provider_class, family)
474
+ errors << {
475
+ provider: provider_name,
476
+ tier: tier,
477
+ model: model_name,
478
+ message: "Model '#{model_name}' not supported by provider '#{provider_name}'",
479
+ fix: suggest_alternative_model(provider_name, tier, model_name)
480
+ }
481
+ end
482
+
483
+ # Check if model exists in registry
484
+ model_info = registry.get_model_info(family)
485
+ unless model_info
486
+ warnings << {
487
+ provider: provider_name,
488
+ tier: tier,
489
+ model: model_name,
490
+ message: "Model family '#{family}' not found in registry (may still work)"
491
+ }
492
+ end
493
+ end
494
+ end
495
+ end
496
+
497
+ {errors: errors, warnings: warnings}
498
+ end
499
+
500
+ def get_provider_class(provider_name)
501
+ class_name = "Aidp::Providers::#{provider_name.capitalize}"
502
+ Object.const_get(class_name)
503
+ rescue NameError
504
+ nil
505
+ end
506
+
507
+ def get_model_family(provider_class, model_name)
508
+ return model_name unless provider_class.respond_to?(:model_family)
509
+ provider_class.model_family(model_name)
510
+ end
511
+
512
+ def provider_supports_model?(provider_class, family)
513
+ return true unless provider_class.respond_to?(:supports_model_family?)
514
+ provider_class.supports_model_family?(family)
515
+ end
516
+
517
+ def generate_tier_fix_suggestion(tier, config)
518
+ # Get a model from registry for this tier
519
+ tier_models = registry.models_for_tier(tier)
520
+ return "Configure a model for this tier in aidp.yml" if tier_models.empty?
521
+
522
+ # Find a model that works with configured providers
523
+ configured_providers = config.configured_providers
524
+ suggested_model = nil
525
+
526
+ tier_models.each do |family|
527
+ configured_providers.each do |provider_name|
528
+ provider_class = get_provider_class(provider_name)
529
+ next unless provider_class
530
+
531
+ if provider_supports_model?(provider_class, family)
532
+ suggested_model = {family: family, provider: provider_name}
533
+ break
534
+ end
535
+ end
536
+ break if suggested_model
537
+ end
538
+
539
+ if suggested_model
540
+ "Add to aidp.yml under providers.#{suggested_model[:provider]}.thinking.tiers.#{tier}.models:\n" \
541
+ " - model: #{suggested_model[:family]}"
542
+ else
543
+ "Configure a model for this tier in aidp.yml"
544
+ end
545
+ end
546
+
547
+ def suggest_alternative_model(provider_name, tier, invalid_model)
548
+ # Get models from registry for this tier and provider
549
+ tier_models = if registry.is_a?(Aidp::Harness::ModelRegistry)
550
+ registry.models_for_tier(tier.to_s)
551
+ else
552
+ []
553
+ end
554
+ provider_class = get_provider_class(provider_name)
555
+ return "Check model name or use a different provider" unless provider_class
556
+
557
+ # Find valid alternatives
558
+ alternatives = tier_models.select do |family|
559
+ provider_supports_model?(provider_class, family)
560
+ end
561
+
562
+ if alternatives.any?
563
+ "Try using: #{alternatives.first(3).join(", ")}"
564
+ else
565
+ "Provider '#{provider_name}' doesn't support any models for tier '#{tier}'"
566
+ end
567
+ end
568
+
569
+ def display_validation_results(issues, warnings)
570
+ if issues.empty? && warnings.empty?
571
+ display_message("✅ Configuration is valid!\n", type: :success)
572
+ display_message("All tiers have models configured", type: :info)
573
+ display_message("All configured models are valid for their providers\n", type: :info)
574
+ return
575
+ end
576
+
577
+ # Display errors
578
+ if issues.any?
579
+ display_message("❌ Found #{issues.size} configuration error#{"s" unless issues.size == 1}:\n", type: :error)
580
+
581
+ issues.each_with_index do |issue, idx|
582
+ display_message("\n#{idx + 1}. #{issue[:message]}", type: :error)
583
+
584
+ if issue[:tier]
585
+ display_message(" Tier: #{issue[:tier]}", type: :info)
586
+ end
587
+
588
+ if issue[:provider]
589
+ display_message(" Provider: #{issue[:provider]}", type: :info)
590
+ end
591
+
592
+ if issue[:model]
593
+ display_message(" Model: #{issue[:model]}", type: :info)
594
+ end
595
+
596
+ if issue[:fix]
597
+ display_message("\n 💡 Suggested fix:", type: :highlight)
598
+ display_message(" #{issue[:fix]}", type: :info)
599
+ end
600
+ end
601
+ display_message("\n", type: :info)
602
+ end
603
+
604
+ # Display warnings
605
+ if warnings.any?
606
+ display_message("⚠️ Found #{warnings.size} warning#{"s" unless warnings.size == 1}:\n", type: :warning)
607
+
608
+ warnings.each_with_index do |warning, idx|
609
+ display_message("\n#{idx + 1}. #{warning[:message]}", type: :warning)
610
+
611
+ if warning[:provider]
612
+ display_message(" Provider: #{warning[:provider]}", type: :info)
613
+ end
614
+
615
+ if warning[:model]
616
+ display_message(" Model: #{warning[:model]}", type: :info)
617
+ end
618
+ end
619
+ display_message("\n", type: :info)
620
+ end
621
+
622
+ # Display helpful tips
623
+ display_message("💡 Run 'aidp models discover' to see available models", type: :info)
624
+ display_message("💡 Run 'aidp models list --tier=<tier>' to see models for a specific tier\n", type: :info)
625
+ end
626
+
627
+ # Lightweight configuration wrapper for CLI validation
628
+ class SimpleConfiguration
629
+ def initialize(providers_section)
630
+ @providers = (providers_section || {}).each_with_object({}) do |(name, cfg), result|
631
+ result[name.to_s] = deep_symbolize(cfg || {})
632
+ end
633
+ end
634
+
635
+ def configured_providers
636
+ @providers.keys
637
+ end
638
+
639
+ def provider_config(name)
640
+ @providers[name.to_s] || {}
641
+ end
642
+
643
+ private
644
+
645
+ def deep_symbolize(value)
646
+ case value
647
+ when Hash
648
+ value.each_with_object({}) do |(key, val), result|
649
+ result_key = key.is_a?(String) ? key.to_sym : key
650
+ result[result_key] = deep_symbolize(val)
651
+ end
652
+ when Array
653
+ value.map { |item| deep_symbolize(item) }
654
+ else
655
+ value
656
+ end
657
+ end
658
+ end
659
+ private_constant :SimpleConfiguration
660
+ end
661
+ end
662
+ end