aidp 0.26.0 → 0.28.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +89 -0
  3. data/lib/aidp/cli/checkpoint_command.rb +198 -0
  4. data/lib/aidp/cli/config_command.rb +71 -0
  5. data/lib/aidp/cli/enhanced_input.rb +2 -0
  6. data/lib/aidp/cli/first_run_wizard.rb +8 -7
  7. data/lib/aidp/cli/harness_command.rb +102 -0
  8. data/lib/aidp/cli/jobs_command.rb +3 -3
  9. data/lib/aidp/cli/mcp_dashboard.rb +4 -3
  10. data/lib/aidp/cli/models_command.rb +661 -0
  11. data/lib/aidp/cli/providers_command.rb +223 -0
  12. data/lib/aidp/cli.rb +45 -464
  13. data/lib/aidp/config.rb +54 -0
  14. data/lib/aidp/daemon/runner.rb +2 -2
  15. data/lib/aidp/debug_mixin.rb +25 -10
  16. data/lib/aidp/execute/agent_signal_parser.rb +22 -0
  17. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  18. data/lib/aidp/execute/checkpoint_display.rb +38 -37
  19. data/lib/aidp/execute/interactive_repl.rb +2 -1
  20. data/lib/aidp/execute/prompt_manager.rb +4 -4
  21. data/lib/aidp/execute/repl_macros.rb +2 -2
  22. data/lib/aidp/execute/steps.rb +94 -1
  23. data/lib/aidp/execute/work_loop_runner.rb +238 -19
  24. data/lib/aidp/execute/workflow_selector.rb +4 -27
  25. data/lib/aidp/firewall/provider_requirements_collector.rb +262 -0
  26. data/lib/aidp/harness/ai_decision_engine.rb +35 -2
  27. data/lib/aidp/harness/config_manager.rb +5 -10
  28. data/lib/aidp/harness/config_schema.rb +8 -0
  29. data/lib/aidp/harness/configuration.rb +40 -2
  30. data/lib/aidp/harness/enhanced_runner.rb +25 -19
  31. data/lib/aidp/harness/error_handler.rb +23 -73
  32. data/lib/aidp/harness/model_cache.rb +269 -0
  33. data/lib/aidp/harness/model_discovery_service.rb +259 -0
  34. data/lib/aidp/harness/model_registry.rb +201 -0
  35. data/lib/aidp/harness/provider_factory.rb +11 -2
  36. data/lib/aidp/harness/runner.rb +5 -0
  37. data/lib/aidp/harness/state_manager.rb +0 -7
  38. data/lib/aidp/harness/thinking_depth_manager.rb +202 -7
  39. data/lib/aidp/harness/ui/enhanced_tui.rb +8 -18
  40. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +0 -18
  41. data/lib/aidp/harness/ui/progress_display.rb +6 -2
  42. data/lib/aidp/harness/user_interface.rb +0 -58
  43. data/lib/aidp/init/runner.rb +7 -2
  44. data/lib/aidp/message_display.rb +0 -46
  45. data/lib/aidp/planning/analyzers/feedback_analyzer.rb +365 -0
  46. data/lib/aidp/planning/builders/agile_plan_builder.rb +387 -0
  47. data/lib/aidp/planning/builders/project_plan_builder.rb +193 -0
  48. data/lib/aidp/planning/generators/gantt_generator.rb +190 -0
  49. data/lib/aidp/planning/generators/iteration_plan_generator.rb +392 -0
  50. data/lib/aidp/planning/generators/legacy_research_planner.rb +473 -0
  51. data/lib/aidp/planning/generators/marketing_report_generator.rb +348 -0
  52. data/lib/aidp/planning/generators/mvp_scope_generator.rb +310 -0
  53. data/lib/aidp/planning/generators/user_test_plan_generator.rb +373 -0
  54. data/lib/aidp/planning/generators/wbs_generator.rb +259 -0
  55. data/lib/aidp/planning/mappers/persona_mapper.rb +163 -0
  56. data/lib/aidp/planning/parsers/document_parser.rb +141 -0
  57. data/lib/aidp/planning/parsers/feedback_data_parser.rb +252 -0
  58. data/lib/aidp/provider_manager.rb +8 -32
  59. data/lib/aidp/providers/adapter.rb +2 -4
  60. data/lib/aidp/providers/aider.rb +264 -0
  61. data/lib/aidp/providers/anthropic.rb +206 -121
  62. data/lib/aidp/providers/base.rb +123 -3
  63. data/lib/aidp/providers/capability_registry.rb +0 -1
  64. data/lib/aidp/providers/codex.rb +75 -70
  65. data/lib/aidp/providers/cursor.rb +87 -59
  66. data/lib/aidp/providers/gemini.rb +57 -60
  67. data/lib/aidp/providers/github_copilot.rb +19 -66
  68. data/lib/aidp/providers/kilocode.rb +35 -80
  69. data/lib/aidp/providers/opencode.rb +35 -80
  70. data/lib/aidp/setup/wizard.rb +555 -8
  71. data/lib/aidp/version.rb +1 -1
  72. data/lib/aidp/watch/build_processor.rb +211 -30
  73. data/lib/aidp/watch/change_request_processor.rb +128 -14
  74. data/lib/aidp/watch/ci_fix_processor.rb +103 -37
  75. data/lib/aidp/watch/ci_log_extractor.rb +258 -0
  76. data/lib/aidp/watch/github_state_extractor.rb +177 -0
  77. data/lib/aidp/watch/implementation_verifier.rb +284 -0
  78. data/lib/aidp/watch/plan_generator.rb +95 -52
  79. data/lib/aidp/watch/plan_processor.rb +7 -6
  80. data/lib/aidp/watch/repository_client.rb +245 -17
  81. data/lib/aidp/watch/review_processor.rb +100 -19
  82. data/lib/aidp/watch/reviewers/base_reviewer.rb +1 -1
  83. data/lib/aidp/watch/runner.rb +181 -29
  84. data/lib/aidp/watch/state_store.rb +22 -1
  85. data/lib/aidp/workflows/definitions.rb +147 -0
  86. data/lib/aidp/workflows/guided_agent.rb +3 -3
  87. data/lib/aidp/workstream_cleanup.rb +245 -0
  88. data/lib/aidp/worktree.rb +19 -0
  89. data/templates/aidp-development.yml.example +2 -2
  90. data/templates/aidp-production.yml.example +3 -3
  91. data/templates/aidp.yml.example +57 -0
  92. data/templates/implementation/generate_tdd_specs.md +213 -0
  93. data/templates/implementation/iterative_implementation.md +122 -0
  94. data/templates/planning/agile/analyze_feedback.md +183 -0
  95. data/templates/planning/agile/generate_iteration_plan.md +179 -0
  96. data/templates/planning/agile/generate_legacy_research_plan.md +171 -0
  97. data/templates/planning/agile/generate_marketing_report.md +162 -0
  98. data/templates/planning/agile/generate_mvp_scope.md +127 -0
  99. data/templates/planning/agile/generate_user_test_plan.md +143 -0
  100. data/templates/planning/agile/ingest_feedback.md +174 -0
  101. data/templates/planning/assemble_project_plan.md +113 -0
  102. data/templates/planning/assign_personas.md +108 -0
  103. data/templates/planning/create_tasks.md +52 -6
  104. data/templates/planning/generate_gantt.md +86 -0
  105. data/templates/planning/generate_wbs.md +85 -0
  106. data/templates/planning/initialize_planning_mode.md +70 -0
  107. data/templates/skills/README.md +2 -2
  108. data/templates/skills/marketing_strategist/SKILL.md +279 -0
  109. data/templates/skills/product_manager/SKILL.md +177 -0
  110. data/templates/skills/ruby_aidp_planning/SKILL.md +497 -0
  111. data/templates/skills/ruby_rspec_tdd/SKILL.md +514 -0
  112. data/templates/skills/ux_researcher/SKILL.md +222 -0
  113. metadata +47 -1
@@ -0,0 +1,661 @@
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
+ tiers_cfg = provider_cfg[:thinking_tiers] || {}
459
+
460
+ # Validate models in each tier
461
+ tiers_cfg.each do |tier, tier_config|
462
+ next unless tier_config[:models]
463
+
464
+ tier_config[:models].each do |model_entry|
465
+ model_name = model_entry.is_a?(Hash) ? model_entry[:model] : model_entry
466
+ next unless model_name
467
+
468
+ # Check if provider supports this model family
469
+ family = get_model_family(provider_class, model_name)
470
+ next unless family
471
+
472
+ unless provider_supports_model?(provider_class, family)
473
+ errors << {
474
+ provider: provider_name,
475
+ tier: tier,
476
+ model: model_name,
477
+ message: "Model '#{model_name}' not supported by provider '#{provider_name}'",
478
+ fix: suggest_alternative_model(provider_name, tier, model_name)
479
+ }
480
+ end
481
+
482
+ # Check if model exists in registry
483
+ model_info = registry.get_model_info(family)
484
+ unless model_info
485
+ warnings << {
486
+ provider: provider_name,
487
+ tier: tier,
488
+ model: model_name,
489
+ message: "Model family '#{family}' not found in registry (may still work)"
490
+ }
491
+ end
492
+ end
493
+ end
494
+ end
495
+
496
+ {errors: errors, warnings: warnings}
497
+ end
498
+
499
+ def get_provider_class(provider_name)
500
+ class_name = "Aidp::Providers::#{provider_name.capitalize}"
501
+ Object.const_get(class_name)
502
+ rescue NameError
503
+ nil
504
+ end
505
+
506
+ def get_model_family(provider_class, model_name)
507
+ return model_name unless provider_class.respond_to?(:model_family)
508
+ provider_class.model_family(model_name)
509
+ end
510
+
511
+ def provider_supports_model?(provider_class, family)
512
+ return true unless provider_class.respond_to?(:supports_model_family?)
513
+ provider_class.supports_model_family?(family)
514
+ end
515
+
516
+ def generate_tier_fix_suggestion(tier, config)
517
+ # Get a model from registry for this tier
518
+ tier_models = registry.models_for_tier(tier)
519
+ return "Configure a model for this tier in aidp.yml" if tier_models.empty?
520
+
521
+ # Find a model that works with configured providers
522
+ configured_providers = config.configured_providers
523
+ suggested_model = nil
524
+
525
+ tier_models.each do |family|
526
+ configured_providers.each do |provider_name|
527
+ provider_class = get_provider_class(provider_name)
528
+ next unless provider_class
529
+
530
+ if provider_supports_model?(provider_class, family)
531
+ suggested_model = {family: family, provider: provider_name}
532
+ break
533
+ end
534
+ end
535
+ break if suggested_model
536
+ end
537
+
538
+ if suggested_model
539
+ "Add to aidp.yml under providers.#{suggested_model[:provider]}.thinking_tiers.#{tier}.models:\n" \
540
+ " - #{suggested_model[:family]}"
541
+ else
542
+ "Configure a model for this tier in aidp.yml"
543
+ end
544
+ end
545
+
546
+ def suggest_alternative_model(provider_name, tier, invalid_model)
547
+ # Get models from registry for this tier and provider
548
+ tier_models = if registry.is_a?(Aidp::Harness::ModelRegistry)
549
+ registry.models_for_tier(tier.to_s)
550
+ else
551
+ []
552
+ end
553
+ provider_class = get_provider_class(provider_name)
554
+ return "Check model name or use a different provider" unless provider_class
555
+
556
+ # Find valid alternatives
557
+ alternatives = tier_models.select do |family|
558
+ provider_supports_model?(provider_class, family)
559
+ end
560
+
561
+ if alternatives.any?
562
+ "Try using: #{alternatives.first(3).join(", ")}"
563
+ else
564
+ "Provider '#{provider_name}' doesn't support any models for tier '#{tier}'"
565
+ end
566
+ end
567
+
568
+ def display_validation_results(issues, warnings)
569
+ if issues.empty? && warnings.empty?
570
+ display_message("✅ Configuration is valid!\n", type: :success)
571
+ display_message("All tiers have models configured", type: :info)
572
+ display_message("All configured models are valid for their providers\n", type: :info)
573
+ return
574
+ end
575
+
576
+ # Display errors
577
+ if issues.any?
578
+ display_message("❌ Found #{issues.size} configuration error#{"s" unless issues.size == 1}:\n", type: :error)
579
+
580
+ issues.each_with_index do |issue, idx|
581
+ display_message("\n#{idx + 1}. #{issue[:message]}", type: :error)
582
+
583
+ if issue[:tier]
584
+ display_message(" Tier: #{issue[:tier]}", type: :info)
585
+ end
586
+
587
+ if issue[:provider]
588
+ display_message(" Provider: #{issue[:provider]}", type: :info)
589
+ end
590
+
591
+ if issue[:model]
592
+ display_message(" Model: #{issue[:model]}", type: :info)
593
+ end
594
+
595
+ if issue[:fix]
596
+ display_message("\n 💡 Suggested fix:", type: :highlight)
597
+ display_message(" #{issue[:fix]}", type: :info)
598
+ end
599
+ end
600
+ display_message("\n", type: :info)
601
+ end
602
+
603
+ # Display warnings
604
+ if warnings.any?
605
+ display_message("⚠️ Found #{warnings.size} warning#{"s" unless warnings.size == 1}:\n", type: :warning)
606
+
607
+ warnings.each_with_index do |warning, idx|
608
+ display_message("\n#{idx + 1}. #{warning[:message]}", type: :warning)
609
+
610
+ if warning[:provider]
611
+ display_message(" Provider: #{warning[:provider]}", type: :info)
612
+ end
613
+
614
+ if warning[:model]
615
+ display_message(" Model: #{warning[:model]}", type: :info)
616
+ end
617
+ end
618
+ display_message("\n", type: :info)
619
+ end
620
+
621
+ # Display helpful tips
622
+ display_message("💡 Run 'aidp models discover' to see available models", type: :info)
623
+ display_message("💡 Run 'aidp models list --tier=<tier>' to see models for a specific tier\n", type: :info)
624
+ end
625
+
626
+ # Lightweight configuration wrapper for CLI validation
627
+ class SimpleConfiguration
628
+ def initialize(providers_section)
629
+ @providers = (providers_section || {}).each_with_object({}) do |(name, cfg), result|
630
+ result[name.to_s] = deep_symbolize(cfg || {})
631
+ end
632
+ end
633
+
634
+ def configured_providers
635
+ @providers.keys
636
+ end
637
+
638
+ def provider_config(name)
639
+ @providers[name.to_s] || {}
640
+ end
641
+
642
+ private
643
+
644
+ def deep_symbolize(value)
645
+ case value
646
+ when Hash
647
+ value.each_with_object({}) do |(key, val), result|
648
+ result_key = key.is_a?(String) ? key.to_sym : key
649
+ result[result_key] = deep_symbolize(val)
650
+ end
651
+ when Array
652
+ value.map { |item| deep_symbolize(item) }
653
+ else
654
+ value
655
+ end
656
+ end
657
+ end
658
+ private_constant :SimpleConfiguration
659
+ end
660
+ end
661
+ end