aidp 0.26.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aidp/cli/checkpoint_command.rb +198 -0
  3. data/lib/aidp/cli/config_command.rb +71 -0
  4. data/lib/aidp/cli/enhanced_input.rb +2 -0
  5. data/lib/aidp/cli/first_run_wizard.rb +8 -7
  6. data/lib/aidp/cli/harness_command.rb +102 -0
  7. data/lib/aidp/cli/jobs_command.rb +3 -3
  8. data/lib/aidp/cli/mcp_dashboard.rb +4 -3
  9. data/lib/aidp/cli/models_command.rb +662 -0
  10. data/lib/aidp/cli/providers_command.rb +223 -0
  11. data/lib/aidp/cli.rb +35 -456
  12. data/lib/aidp/daemon/runner.rb +2 -2
  13. data/lib/aidp/debug_mixin.rb +2 -9
  14. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  15. data/lib/aidp/execute/checkpoint_display.rb +38 -37
  16. data/lib/aidp/execute/interactive_repl.rb +2 -1
  17. data/lib/aidp/execute/prompt_manager.rb +4 -4
  18. data/lib/aidp/execute/work_loop_runner.rb +29 -2
  19. data/lib/aidp/execute/workflow_selector.rb +2 -2
  20. data/lib/aidp/harness/config_manager.rb +5 -5
  21. data/lib/aidp/harness/configuration.rb +32 -2
  22. data/lib/aidp/harness/enhanced_runner.rb +24 -15
  23. data/lib/aidp/harness/error_handler.rb +26 -5
  24. data/lib/aidp/harness/model_cache.rb +269 -0
  25. data/lib/aidp/harness/model_discovery_service.rb +259 -0
  26. data/lib/aidp/harness/model_registry.rb +201 -0
  27. data/lib/aidp/harness/runner.rb +5 -0
  28. data/lib/aidp/harness/thinking_depth_manager.rb +223 -7
  29. data/lib/aidp/message_display.rb +0 -46
  30. data/lib/aidp/providers/adapter.rb +2 -4
  31. data/lib/aidp/providers/anthropic.rb +141 -128
  32. data/lib/aidp/providers/base.rb +98 -2
  33. data/lib/aidp/providers/capability_registry.rb +0 -1
  34. data/lib/aidp/providers/codex.rb +49 -67
  35. data/lib/aidp/providers/cursor.rb +71 -59
  36. data/lib/aidp/providers/gemini.rb +44 -60
  37. data/lib/aidp/providers/github_copilot.rb +2 -66
  38. data/lib/aidp/providers/kilocode.rb +24 -80
  39. data/lib/aidp/providers/opencode.rb +24 -80
  40. data/lib/aidp/setup/wizard.rb +345 -8
  41. data/lib/aidp/version.rb +1 -1
  42. data/lib/aidp/watch/plan_generator.rb +93 -14
  43. data/lib/aidp/watch/review_processor.rb +3 -3
  44. data/lib/aidp/workflows/guided_agent.rb +3 -3
  45. data/templates/aidp-development.yml.example +2 -2
  46. data/templates/aidp-production.yml.example +3 -3
  47. metadata +9 -1
@@ -9,6 +9,7 @@ require "json"
9
9
 
10
10
  require_relative "../util"
11
11
  require_relative "../config/paths"
12
+ require_relative "../harness/capability_registry"
12
13
  require_relative "provider_registry"
13
14
  require_relative "devcontainer/parser"
14
15
  require_relative "devcontainer/generator"
@@ -24,6 +25,11 @@ module Aidp
24
25
  SCHEMA_VERSION = 1
25
26
  DEVCONTAINER_COMPONENT = "setup_wizard.devcontainer"
26
27
 
28
+ DEFAULT_AUTOCONFIG_TIERS = %w[mini standard pro].freeze
29
+ LEGACY_TIER_ALIASES = {
30
+ advanced: :pro
31
+ }.freeze
32
+
27
33
  attr_reader :project_dir, :prompt, :dry_run
28
34
 
29
35
  def initialize(project_dir = Dir.pwd, prompt: nil, dry_run: false)
@@ -38,11 +44,13 @@ module Aidp
38
44
 
39
45
  def run
40
46
  display_welcome
41
- # Normalize any legacy or label-based model_family entries before prompting
47
+ # Normalize any legacy tier/model_family entries before prompting
42
48
  normalize_existing_model_families!
49
+ normalize_existing_thinking_tiers!
43
50
  return @saved if skip_wizard?
44
51
 
45
52
  configure_providers
53
+ configure_thinking_tiers
46
54
  configure_work_loop
47
55
  configure_branching
48
56
  configure_artifacts
@@ -51,6 +59,9 @@ module Aidp
51
59
  configure_modes
52
60
  configure_devcontainer
53
61
 
62
+ # Finalize any background model discovery
63
+ finalize_background_discovery
64
+
54
65
  yaml_content = generate_yaml
55
66
  display_preview(yaml_content)
56
67
  display_diff(yaml_content) if @existing_config.any?
@@ -279,6 +290,249 @@ module Aidp
279
290
 
280
291
  # Removed MCP configuration step (MCP now expected to be provider-specific if used)
281
292
 
293
+ # -------------------------------------------
294
+ # Thinking tier configuration (automated model discovery)
295
+ # -------------------------------------------
296
+ def configure_thinking_tiers
297
+ prompt.say("\n🧠 Thinking Tier Configuration")
298
+ prompt.say("-" * 40)
299
+
300
+ # Get configured providers
301
+ primary_provider = get([:harness, :default_provider])
302
+ fallback_providers = Array(get([:harness, :fallback_providers]))
303
+ all_providers = ([primary_provider] + fallback_providers).compact.uniq
304
+
305
+ if all_providers.empty?
306
+ prompt.warn("⚠️ No providers configured. Skipping tier configuration.")
307
+ return
308
+ end
309
+
310
+ # Check if user wants to use automated discovery
311
+ existing_tiers = get([:thinking, :tiers])
312
+ if existing_tiers && !existing_tiers.empty?
313
+ prompt.say("📝 Found existing tier configuration")
314
+ unless prompt.yes?("Would you like to update it with discovered models?", default: false)
315
+ return
316
+ end
317
+ elsif !prompt.yes?("Auto-configure thinking tiers with discovered models?", default: true)
318
+ prompt.say("💡 You can run 'aidp models discover' later to see available models")
319
+ return
320
+ end
321
+
322
+ # Run model discovery
323
+ prompt.say("\n🔍 Discovering available models...")
324
+ discovered_models = discover_models_for_providers(all_providers)
325
+
326
+ if discovered_models.empty?
327
+ prompt.warn("⚠️ No models discovered. Ensure provider CLIs are installed.")
328
+ prompt.say("💡 You can configure tiers manually or run 'aidp models discover' later")
329
+ return
330
+ end
331
+
332
+ # Display discovered models
333
+ display_discovered_models(discovered_models)
334
+
335
+ # Generate tier configuration
336
+ tier_config = generate_tier_configuration(discovered_models, primary_provider)
337
+
338
+ # Show preview
339
+ prompt.say("\n📋 Proposed tier configuration:")
340
+ display_tier_preview(tier_config)
341
+
342
+ # Confirm and save
343
+ if prompt.yes?("\nSave this tier configuration?", default: true)
344
+ set([:thinking, :tiers], tier_config)
345
+ prompt.ok("✅ Thinking tiers configured successfully")
346
+ else
347
+ prompt.say("💡 Skipped tier configuration. You can run 'aidp models discover' later")
348
+ end
349
+ end
350
+
351
+ # Trigger background model discovery for a provider
352
+ # Runs asynchronously and caches results without blocking the wizard
353
+ def trigger_background_discovery(provider_name)
354
+ return unless provider_available_for_discovery?(provider_name)
355
+
356
+ # Store reference to display notification later
357
+ @discovery_threads ||= []
358
+
359
+ thread = Thread.new do
360
+ discover_and_cache_models(provider_name)
361
+ rescue => e
362
+ Aidp.log_debug("setup_wizard", "background discovery failed",
363
+ provider: provider_name, error: e.message)
364
+ end
365
+
366
+ @discovery_threads << {thread: thread, provider: provider_name}
367
+ end
368
+
369
+ # Check if provider CLI is available for discovery
370
+ def provider_available_for_discovery?(provider_name)
371
+ provider_class = get_provider_class(provider_name)
372
+ return false unless provider_class
373
+
374
+ provider_class.respond_to?(:available?) && provider_class.available?
375
+ rescue => e
376
+ Aidp.log_debug("setup_wizard", "provider availability check failed",
377
+ provider: provider_name, error: e.message)
378
+ false
379
+ end
380
+
381
+ # Perform model discovery and cache results
382
+ def discover_and_cache_models(provider_name)
383
+ require_relative "../harness/model_discovery_service"
384
+
385
+ service = Aidp::Harness::ModelDiscoveryService.new
386
+ models = service.discover_models(provider_name, use_cache: false)
387
+
388
+ if models.any?
389
+ Aidp.log_info("setup_wizard", "discovered models in background",
390
+ provider: provider_name, count: models.size)
391
+ end
392
+
393
+ models
394
+ rescue => e
395
+ Aidp.log_debug("setup_wizard", "background discovery failed",
396
+ provider: provider_name, error: e.message)
397
+ []
398
+ end
399
+
400
+ # Get provider class for discovery
401
+ def get_provider_class(provider_name)
402
+ class_name = "Aidp::Providers::#{provider_name.capitalize}"
403
+ Object.const_get(class_name)
404
+ rescue NameError
405
+ nil
406
+ end
407
+
408
+ # Wait for background discovery to complete and show notifications
409
+ #
410
+ # @param timeout [Numeric] Maximum seconds to wait per thread (default: 5)
411
+ def finalize_background_discovery(timeout: 5)
412
+ return unless @discovery_threads&.any?
413
+
414
+ @discovery_threads.each do |entry|
415
+ thread = entry[:thread]
416
+ provider = entry[:provider]
417
+
418
+ # Wait up to timeout seconds for discovery to complete
419
+ thread.join(timeout)
420
+
421
+ if thread.alive?
422
+ Aidp.log_debug("setup_wizard", "discovery timeout, killing thread",
423
+ provider: provider)
424
+ # Kill thread to prevent hanging
425
+ thread.kill
426
+ thread.join(0.1) # Brief wait for cleanup
427
+ else
428
+ # Discovery completed - show notification
429
+ begin
430
+ require_relative "../harness/model_cache"
431
+ cache = Aidp::Harness::ModelCache.new
432
+ cached_models = cache.get_cached_models(provider)
433
+
434
+ if cached_models&.any?
435
+ prompt.say(" 💾 Discovered #{cached_models.size} model#{"s" unless cached_models.size == 1} for #{provider}")
436
+ end
437
+ rescue => e
438
+ Aidp.log_debug("setup_wizard", "failed to check cached models",
439
+ provider: provider, error: e.message)
440
+ end
441
+ end
442
+ end
443
+
444
+ @discovery_threads = []
445
+ end
446
+
447
+ def discover_models_for_providers(providers)
448
+ require_relative "../harness/model_discovery_service"
449
+
450
+ service = Aidp::Harness::ModelDiscoveryService.new
451
+ all_models = {}
452
+
453
+ providers.each do |provider|
454
+ models = service.discover_models(provider, use_cache: true)
455
+ all_models[provider] = models if models.any?
456
+ rescue => e
457
+ Aidp.log_debug("setup_wizard", "discovery failed", provider: provider, error: e.message)
458
+ # Continue with other providers
459
+ end
460
+
461
+ all_models
462
+ end
463
+
464
+ def display_discovered_models(discovered_models)
465
+ discovered_models.each do |provider, models|
466
+ prompt.say("\n✓ Found #{models.size} models for #{provider}:")
467
+ by_tier = models.group_by { |m| m[:tier] }
468
+ valid_thinking_tiers.each do |tier|
469
+ tier_models = by_tier[tier] || []
470
+ next if tier_models.empty?
471
+
472
+ prompt.say(" #{tier.capitalize} tier: #{tier_models.size} model#{"s" unless tier_models.size == 1}")
473
+ end
474
+ end
475
+ end
476
+
477
+ def generate_tier_configuration(discovered_models, primary_provider)
478
+ tier_config = {}
479
+
480
+ # Configure the three most common tiers: mini, standard, and pro
481
+ DEFAULT_AUTOCONFIG_TIERS.each do |tier|
482
+ tier_models = []
483
+
484
+ # Collect primary provider models first (if available)
485
+ primary_models = find_models_for_tier(discovered_models[primary_provider], tier)
486
+ tier_models.concat(primary_models) if primary_models&.any?
487
+
488
+ # Add models from other providers
489
+ discovered_models.each do |provider, models|
490
+ next if provider == primary_provider
491
+ provider_models = find_models_for_tier(models, tier)
492
+ tier_models.concat(provider_models) if provider_models&.any?
493
+ end
494
+
495
+ # Add to config if we found any models for this tier
496
+ if tier_models.any?
497
+ tier_config[tier.to_sym] = {
498
+ models: tier_models.map { |m|
499
+ {
500
+ provider: m[:provider],
501
+ model: m[:name]
502
+ }
503
+ }
504
+ }
505
+ end
506
+ end
507
+
508
+ tier_config
509
+ end
510
+
511
+ def find_model_for_tier(models, target_tier)
512
+ return nil unless models
513
+
514
+ models.find { |m| m[:tier] == target_tier }
515
+ end
516
+
517
+ def find_models_for_tier(models, target_tier)
518
+ return [] unless models
519
+
520
+ models.select { |m| m[:tier] == target_tier }
521
+ end
522
+
523
+ def display_tier_preview(tier_config)
524
+ return if tier_config.empty?
525
+
526
+ tier_config.each do |tier, config|
527
+ models = config[:models] || []
528
+ prompt.say(" #{tier}:")
529
+ models.each do |model_entry|
530
+ prompt.say(" - provider: #{model_entry[:provider]}")
531
+ prompt.say(" model: #{model_entry[:model]}")
532
+ end
533
+ end
534
+ end
535
+
282
536
  # -------------------------------------------
283
537
  # Work loop configuration
284
538
  # -------------------------------------------
@@ -670,8 +924,18 @@ module Aidp
670
924
  prompt.say("\n📋 Non-functional requirements & preferred libraries")
671
925
  prompt.say("-" * 40)
672
926
 
673
- return delete_path([:nfrs]) unless prompt.yes?("Configure NFRs?", default: true)
927
+ # Check existing configuration for previous choice
928
+ existing_configure = @config.dig(:nfrs, :configure)
929
+ default_configure = existing_configure.nil? || existing_configure
930
+
931
+ configure = prompt.yes?("Configure NFRs?", default: default_configure)
932
+
933
+ unless configure
934
+ Aidp.log_debug("setup_wizard.nfrs", "opt_out")
935
+ return set([:nfrs, :configure], false)
936
+ end
674
937
 
938
+ set([:nfrs, :configure], true)
675
939
  categories = %i[performance security reliability accessibility internationalization]
676
940
  categories.each do |category|
677
941
  existing = get([:nfrs, category])
@@ -835,7 +1099,7 @@ module Aidp
835
1099
 
836
1100
  def configure_watch_labels
837
1101
  prompt.say("\n🏷️ Watch mode label configuration")
838
- prompt.say(" Configure GitHub issue labels that trigger watch mode actions")
1102
+ prompt.say(" Configure GitHub issue and PR labels that trigger watch mode actions")
839
1103
  existing = get([:watch, :labels]) || {}
840
1104
 
841
1105
  plan_trigger = ask_with_default(
@@ -858,11 +1122,29 @@ module Aidp
858
1122
  existing[:build_trigger] || "aidp-build"
859
1123
  )
860
1124
 
1125
+ review_trigger = ask_with_default(
1126
+ "Label to trigger code review",
1127
+ existing[:review_trigger] || "aidp-review"
1128
+ )
1129
+
1130
+ ci_fix_trigger = ask_with_default(
1131
+ "Label to trigger CI remediation",
1132
+ existing[:ci_fix_trigger] || "aidp-fix-ci"
1133
+ )
1134
+
1135
+ change_request_trigger = ask_with_default(
1136
+ "Label to trigger PR change implementation",
1137
+ existing[:change_request_trigger] || "aidp-request-changes"
1138
+ )
1139
+
861
1140
  set([:watch, :labels], {
862
1141
  plan_trigger: plan_trigger,
863
1142
  needs_input: needs_input,
864
1143
  ready_to_build: ready_to_build,
865
- build_trigger: build_trigger
1144
+ build_trigger: build_trigger,
1145
+ review_trigger: review_trigger,
1146
+ ci_fix_trigger: ci_fix_trigger,
1147
+ change_request_trigger: change_request_trigger
866
1148
  })
867
1149
  end
868
1150
 
@@ -953,9 +1235,9 @@ module Aidp
953
1235
  def show_next_steps
954
1236
  prompt.say("\n🎉 Setup complete!")
955
1237
  prompt.say("\nNext steps:")
956
- prompt.say(" 1. Export provider API keys as environment variables.")
957
- prompt.say(" 2. Run 'aidp init' to analyze the project.")
958
- prompt.say(" 3. Run 'aidp execute' to start a work loop.")
1238
+ prompt.say(" 1. Configure provider tools (set required API keys or connections).")
1239
+ prompt.say(" 2. Run 'aidp' to start a work loop.")
1240
+ prompt.say(" 3. Run 'aidp watch <owner/repo>' to enable watch mode automation.")
959
1241
  prompt.say("")
960
1242
  end
961
1243
 
@@ -1225,6 +1507,9 @@ module Aidp
1225
1507
  # Enhance messaging with display name when available
1226
1508
  display_name = discover_available_providers.invert.fetch(provider_name, provider_name)
1227
1509
  prompt.say(" • #{action_word.capitalize} provider '#{display_name}' (#{provider_name}) with billing type '#{provider_type}' and model family '#{model_family}'")
1510
+
1511
+ # Trigger background model discovery for newly added/updated provider
1512
+ trigger_background_discovery(provider_name) unless @dry_run
1228
1513
  end
1229
1514
 
1230
1515
  def edit_or_remove_provider(provider_name, primary_provider, fallbacks)
@@ -1335,6 +1620,44 @@ module Aidp
1335
1620
  end
1336
1621
  end
1337
1622
 
1623
+ def normalize_existing_thinking_tiers!
1624
+ tiers_cfg = @config.dig(:thinking, :tiers)
1625
+ return unless tiers_cfg.is_a?(Hash)
1626
+
1627
+ LEGACY_TIER_ALIASES.each do |legacy, canonical|
1628
+ next unless tiers_cfg.key?(legacy)
1629
+
1630
+ legacy_cfg = tiers_cfg.delete(legacy) || {}
1631
+ canonical_cfg = tiers_cfg[canonical] || {}
1632
+ merged_models = merge_tier_models(canonical_cfg[:models], legacy_cfg[:models])
1633
+ tiers_cfg[canonical] = canonical_cfg.merge(models: merged_models)
1634
+ @warnings << "Normalized thinking tier '#{legacy}' to '#{canonical}'"
1635
+ end
1636
+
1637
+ valid = valid_thinking_tiers
1638
+ tiers_cfg.keys.each do |tier|
1639
+ next if valid.include?(tier.to_s)
1640
+
1641
+ tiers_cfg.delete(tier)
1642
+ @warnings << "Removed unsupported thinking tier '#{tier}' from configuration"
1643
+ end
1644
+ end
1645
+
1646
+ def merge_tier_models(existing_models, new_models)
1647
+ combined = []
1648
+ (Array(existing_models) + Array(new_models)).each do |entry|
1649
+ next unless entry.is_a?(Hash)
1650
+ provider = entry[:provider]
1651
+ model = entry[:model]
1652
+ next unless provider && model
1653
+
1654
+ unless combined.any? { |m| m[:provider] == provider && m[:model] == model }
1655
+ combined << entry
1656
+ end
1657
+ end
1658
+ combined
1659
+ end
1660
+
1338
1661
  def load_existing_config
1339
1662
  return {} unless File.exist?(config_path)
1340
1663
  YAML.safe_load_file(config_path, permitted_classes: [Time]) || {}
@@ -1468,6 +1791,12 @@ module Aidp
1468
1791
  File.exist?(File.join(project_dir, relative_path))
1469
1792
  end
1470
1793
 
1794
+ def valid_thinking_tiers
1795
+ Aidp::Harness::CapabilityRegistry::VALID_TIERS
1796
+ rescue NameError
1797
+ %w[mini standard thinking pro max]
1798
+ end
1799
+
1471
1800
  def configure_devcontainer
1472
1801
  prompt.say("\n🐳 Devcontainer Configuration")
1473
1802
  Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.start")
@@ -1482,10 +1811,18 @@ module Aidp
1482
1811
  path: parser.detect)
1483
1812
  end
1484
1813
 
1814
+ # Check existing configuration for previous choice
1815
+ existing_manage = @config.dig(:devcontainer, :manage)
1816
+ default_manage = if existing_manage.nil?
1817
+ existing_devcontainer ? true : false
1818
+ else
1819
+ existing_manage
1820
+ end
1821
+
1485
1822
  # Ask if user wants AIDP to manage devcontainer
1486
1823
  manage = prompt.yes?(
1487
1824
  "Would you like AIDP to manage your devcontainer configuration?",
1488
- default: (@config.dig(:devcontainer, :manage) || existing_devcontainer) ? true : false
1825
+ default: default_manage
1489
1826
  )
1490
1827
 
1491
1828
  unless manage
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.26.0"
4
+ VERSION = "0.27.0"
5
5
  end
@@ -29,32 +29,103 @@ module Aidp
29
29
  def initialize(provider_name: nil, verbose: false)
30
30
  @provider_name = provider_name
31
31
  @verbose = verbose
32
+ @providers_attempted = []
32
33
  end
33
34
 
34
35
  def generate(issue)
35
- provider = resolve_provider
36
- if provider
37
- generate_with_provider(provider, issue)
38
- else
39
- display_message("⚠️ No active provider available. Falling back to heuristic plan.", type: :warn)
40
- heuristic_plan(issue)
36
+ Aidp.log_debug("plan_generator", "generate.start", provider: @provider_name, issue: issue[:number])
37
+
38
+ # Try providers in fallback chain order
39
+ providers_to_try = build_provider_fallback_chain
40
+ Aidp.log_debug("plan_generator", "fallback_chain", providers: providers_to_try, count: providers_to_try.size)
41
+
42
+ providers_to_try.each do |provider_name|
43
+ next if @providers_attempted.include?(provider_name)
44
+
45
+ Aidp.log_debug("plan_generator", "trying_provider", provider: provider_name, attempted: @providers_attempted)
46
+
47
+ provider = resolve_provider(provider_name)
48
+ unless provider
49
+ Aidp.log_debug("plan_generator", "provider_unavailable", provider: provider_name, reason: "not resolved")
50
+ @providers_attempted << provider_name
51
+ next
52
+ end
53
+
54
+ begin
55
+ Aidp.log_info("plan_generator", "generate_with_provider", provider: provider_name, issue: issue[:number])
56
+ result = generate_with_provider(provider, issue, provider_name)
57
+ if result
58
+ Aidp.log_info("plan_generator", "generation_success", provider: provider_name, issue: issue[:number])
59
+ return result
60
+ end
61
+
62
+ # Provider returned nil - try next provider
63
+ Aidp.log_warn("plan_generator", "provider_returned_nil", provider: provider_name)
64
+ @providers_attempted << provider_name
65
+ rescue => e
66
+ # Log error and try next provider in chain
67
+ Aidp.log_warn("plan_generator", "provider_failed", provider: provider_name, error: e.message, error_class: e.class.name)
68
+ @providers_attempted << provider_name
69
+ end
41
70
  end
71
+
72
+ # All providers exhausted, fall back to heuristic
73
+ Aidp.log_warn("plan_generator", "all_providers_exhausted", attempted: @providers_attempted, falling_back: "heuristic")
74
+ display_message("⚠️ All providers unavailable or failed. Falling back to heuristic plan.", type: :warn)
75
+ heuristic_plan(issue)
42
76
  rescue => e
43
- display_message("⚠️ Plan generation failed (#{e.message}). Using heuristic.", type: :warn)
77
+ Aidp.log_error("plan_generator", "generation_failed_unexpectedly", error: e.message, backtrace: e.backtrace&.first(3))
78
+ display_message("⚠️ Plan generation failed unexpectedly (#{e.message}). Using heuristic.", type: :warn)
44
79
  heuristic_plan(issue)
45
80
  end
46
81
 
47
82
  private
48
83
 
49
- def resolve_provider
50
- provider_name = @provider_name || detect_default_provider
84
+ def build_provider_fallback_chain
85
+ # Start with specified provider or default
86
+ primary_provider = @provider_name || detect_default_provider
87
+ providers = []
88
+
89
+ # Add primary provider first
90
+ providers << primary_provider if primary_provider
91
+
92
+ # Try to get fallback chain from config
93
+ begin
94
+ config_manager = Aidp::Harness::ConfigManager.new(Dir.pwd)
95
+ fallback_providers = config_manager.fallback_providers || []
96
+
97
+ # Add fallback providers that aren't already in the list
98
+ fallback_providers.each do |fallback|
99
+ providers << fallback unless providers.include?(fallback)
100
+ end
101
+ rescue => e
102
+ Aidp.log_debug("plan_generator", "config_fallback_unavailable", error: e.message)
103
+ end
104
+
105
+ # If we still have no providers, add cursor as last resort
106
+ providers << "cursor" if providers.empty?
107
+
108
+ # Remove duplicates while preserving order
109
+ providers.uniq
110
+ end
111
+
112
+ def resolve_provider(provider_name = nil)
113
+ provider_name ||= @provider_name || detect_default_provider
51
114
  return nil unless provider_name
52
115
 
116
+ Aidp.log_debug("plan_generator", "resolve_provider", provider: provider_name)
117
+
53
118
  provider = Aidp::ProviderManager.get_provider(provider_name, use_harness: false)
54
- return provider if provider&.available?
55
119
 
120
+ if provider&.available?
121
+ Aidp.log_debug("plan_generator", "provider_resolved", provider: provider_name, available: true)
122
+ return provider
123
+ end
124
+
125
+ Aidp.log_debug("plan_generator", "provider_not_available", provider: provider_name, available: provider&.available?)
56
126
  nil
57
127
  rescue => e
128
+ Aidp.log_warn("plan_generator", "resolve_provider_failed", provider: provider_name, error: e.message)
58
129
  display_message("⚠️ Failed to resolve provider #{provider_name}: #{e.message}", type: :warn)
59
130
  nil
60
131
  end
@@ -66,9 +137,11 @@ module Aidp
66
137
  "cursor"
67
138
  end
68
139
 
69
- def generate_with_provider(provider, issue)
140
+ def generate_with_provider(provider, issue, provider_name = "unknown")
70
141
  payload = build_prompt(issue)
71
142
 
143
+ Aidp.log_debug("plan_generator", "sending_to_provider", provider: provider_name, prompt_length: payload.length)
144
+
72
145
  if @verbose
73
146
  display_message("\n--- Plan Generation Prompt ---", type: :muted)
74
147
  display_message(payload.strip, type: :muted)
@@ -77,6 +150,8 @@ module Aidp
77
150
 
78
151
  response = provider.send_message(prompt: payload)
79
152
 
153
+ Aidp.log_debug("plan_generator", "provider_response_received", provider: provider_name, response_length: response&.length || 0)
154
+
80
155
  if @verbose
81
156
  display_message("\n--- Provider Response ---", type: :muted)
82
157
  display_message(response.strip, type: :muted)
@@ -85,10 +160,14 @@ module Aidp
85
160
 
86
161
  parsed = parse_structured_response(response)
87
162
 
88
- return parsed if parsed
163
+ if parsed
164
+ Aidp.log_debug("plan_generator", "response_parsed", provider: provider_name, has_summary: !parsed[:summary].to_s.empty?, tasks_count: parsed[:tasks]&.size || 0)
165
+ return parsed
166
+ end
89
167
 
90
- display_message("⚠️ Unable to parse provider response. Using heuristic plan.", type: :warn)
91
- heuristic_plan(issue)
168
+ Aidp.log_warn("plan_generator", "parse_failed", provider: provider_name)
169
+ display_message("⚠️ Unable to parse #{provider_name} response. Trying next provider.", type: :warn)
170
+ nil
92
171
  end
93
172
 
94
173
  def build_prompt(issue)
@@ -23,7 +23,7 @@ module Aidp
23
23
 
24
24
  attr_reader :review_label
25
25
 
26
- def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, verbose: false)
26
+ def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, verbose: false, reviewers: nil)
27
27
  @repository_client = repository_client
28
28
  @state_store = state_store
29
29
  @provider_name = provider_name
@@ -33,8 +33,8 @@ module Aidp
33
33
  # Load label configuration
34
34
  @review_label = label_config[:review_trigger] || label_config["review_trigger"] || DEFAULT_REVIEW_LABEL
35
35
 
36
- # Initialize reviewers
37
- @reviewers = [
36
+ # Initialize reviewers (allow dependency injection for testing)
37
+ @reviewers = reviewers || [
38
38
  Reviewers::SeniorDevReviewer.new(provider_name: provider_name),
39
39
  Reviewers::SecurityReviewer.new(provider_name: provider_name),
40
40
  Reviewers::PerformanceReviewer.new(provider_name: provider_name)
@@ -19,7 +19,7 @@ module Aidp
19
19
 
20
20
  class ConversationError < StandardError; end
21
21
 
22
- def initialize(project_dir, prompt: nil, use_enhanced_input: true, verbose: false)
22
+ def initialize(project_dir, prompt: nil, use_enhanced_input: true, verbose: false, config_manager: nil, provider_manager: nil)
23
23
  @project_dir = project_dir
24
24
 
25
25
  # Use EnhancedInput with Reline for full readline-style key bindings
@@ -29,8 +29,8 @@ module Aidp
29
29
  prompt || TTY::Prompt.new
30
30
  end
31
31
 
32
- @config_manager = Aidp::Harness::ConfigManager.new(project_dir)
33
- @provider_manager = Aidp::Harness::ProviderManager.new(@config_manager, prompt: @prompt)
32
+ @config_manager = config_manager || Aidp::Harness::ConfigManager.new(project_dir)
33
+ @provider_manager = provider_manager || Aidp::Harness::ProviderManager.new(@config_manager, prompt: @prompt)
34
34
  @conversation_history = []
35
35
  @user_input = {}
36
36
  @verbose = verbose
@@ -137,7 +137,7 @@ providers:
137
137
  file_upload: true
138
138
  code_generation: true
139
139
  analysis: true
140
- streaming: true
140
+ supports_json_mode: true
141
141
 
142
142
  # Monitoring configuration (enhanced for development)
143
143
  monitoring:
@@ -243,7 +243,7 @@ providers:
243
243
  code_generation: true
244
244
  analysis: true
245
245
  vision: true
246
- streaming: true
246
+ supports_json_mode: true
247
247
  function_calling: true
248
248
  tool_use: true
249
249