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
@@ -8,17 +8,18 @@ module Aidp
8
8
  class CheckpointDisplay
9
9
  include Aidp::MessageDisplay
10
10
 
11
- def initialize
11
+ def initialize(prompt: nil)
12
12
  @pastel = Pastel.new
13
+ @prompt = prompt || TTY::Prompt.new
13
14
  end
14
15
 
15
16
  # Display a checkpoint during work loop iteration
16
17
  def display_checkpoint(checkpoint_data, show_details: false)
17
18
  return unless checkpoint_data
18
19
 
19
- puts
20
- puts @pastel.bold("📊 Checkpoint - Iteration #{checkpoint_data[:iteration]}")
21
- puts @pastel.dim("─" * 60)
20
+ @prompt.say("")
21
+ @prompt.say(@pastel.bold("📊 Checkpoint - Iteration #{checkpoint_data[:iteration]}"))
22
+ @prompt.say(@pastel.dim("─" * 60))
22
23
 
23
24
  display_metrics(checkpoint_data[:metrics])
24
25
  display_status(checkpoint_data[:status])
@@ -27,52 +28,52 @@ module Aidp
27
28
  display_trends(checkpoint_data[:trends]) if checkpoint_data[:trends]
28
29
  end
29
30
 
30
- puts @pastel.dim("─" * 60)
31
- puts
31
+ @prompt.say(@pastel.dim("─" * 60))
32
+ @prompt.say("")
32
33
  end
33
34
 
34
35
  # Display progress summary with trends
35
36
  def display_progress_summary(summary)
36
37
  return unless summary
37
38
 
38
- puts
39
- puts @pastel.bold("📈 Progress Summary")
40
- puts @pastel.dim("=" * 60)
39
+ @prompt.say("")
40
+ @prompt.say(@pastel.bold("📈 Progress Summary"))
41
+ @prompt.say(@pastel.dim("=" * 60))
41
42
 
42
43
  current = summary[:current]
43
- puts "Step: #{@pastel.cyan(current[:step_name])}"
44
- puts "Iteration: #{current[:iteration]}"
45
- puts "Status: #{format_status(current[:status])}"
46
- puts
44
+ @prompt.say("Step: #{@pastel.cyan(current[:step_name])}")
45
+ @prompt.say("Iteration: #{current[:iteration]}")
46
+ @prompt.say("Status: #{format_status(current[:status])}")
47
+ @prompt.say("")
47
48
 
48
- puts @pastel.bold("Current Metrics:")
49
+ @prompt.say(@pastel.bold("Current Metrics:"))
49
50
  display_metrics(current[:metrics])
50
51
 
51
52
  if summary[:trends]
52
- puts
53
- puts @pastel.bold("Trends:")
53
+ @prompt.say("")
54
+ @prompt.say(@pastel.bold("Trends:"))
54
55
  display_trends(summary[:trends])
55
56
  end
56
57
 
57
58
  if summary[:quality_score]
58
- puts
59
+ @prompt.say("")
59
60
  display_quality_score(summary[:quality_score])
60
61
  end
61
62
 
62
- puts @pastel.dim("=" * 60)
63
- puts
63
+ @prompt.say(@pastel.dim("=" * 60))
64
+ @prompt.say("")
64
65
  end
65
66
 
66
67
  # Display checkpoint history as a table
67
68
  def display_checkpoint_history(history, limit: 10)
68
69
  return if history.empty?
69
70
 
70
- puts
71
- puts @pastel.bold("📜 Checkpoint History (Last #{[limit, history.size].min})")
72
- puts @pastel.dim("=" * 80)
71
+ @prompt.say("")
72
+ @prompt.say(@pastel.bold("📜 Checkpoint History (Last #{[limit, history.size].min})"))
73
+ @prompt.say(@pastel.dim("=" * 80))
73
74
 
74
75
  # Table header
75
- puts format_table_row([
76
+ @prompt.say(format_table_row([
76
77
  "Iteration",
77
78
  "Time",
78
79
  "LOC",
@@ -80,15 +81,15 @@ module Aidp
80
81
  "Quality",
81
82
  "PRD Progress",
82
83
  "Status"
83
- ], header: true)
84
- puts @pastel.dim("-" * 80)
84
+ ], header: true))
85
+ @prompt.say(@pastel.dim("-" * 80))
85
86
 
86
87
  # Table rows
87
88
  history.last(limit).each do |checkpoint|
88
89
  metrics = checkpoint[:metrics]
89
90
  timestamp = Time.parse(checkpoint[:timestamp]).strftime("%H:%M:%S")
90
91
 
91
- puts format_table_row([
92
+ @prompt.say(format_table_row([
92
93
  checkpoint[:iteration].to_s,
93
94
  timestamp,
94
95
  metrics[:lines_of_code].to_s,
@@ -96,11 +97,11 @@ module Aidp
96
97
  "#{metrics[:code_quality]}%",
97
98
  "#{metrics[:prd_task_progress]}%",
98
99
  format_status(checkpoint[:status])
99
- ])
100
+ ]))
100
101
  end
101
102
 
102
- puts @pastel.dim("=" * 80)
103
- puts
103
+ @prompt.say(@pastel.dim("=" * 80))
104
+ @prompt.say("")
104
105
  end
105
106
 
106
107
  # Display inline progress indicator (for work loop)
@@ -124,15 +125,15 @@ module Aidp
124
125
  private
125
126
 
126
127
  def display_metrics(metrics)
127
- puts " Lines of Code: #{@pastel.yellow(metrics[:lines_of_code].to_s)}"
128
- puts " Test Coverage: #{format_percentage_with_color(metrics[:test_coverage])}"
129
- puts " Code Quality: #{format_percentage_with_color(metrics[:code_quality])}"
130
- puts " PRD Task Progress: #{format_percentage_with_color(metrics[:prd_task_progress])}"
131
- puts " File Count: #{metrics[:file_count]}"
128
+ @prompt.say(" Lines of Code: #{@pastel.yellow(metrics[:lines_of_code].to_s)}")
129
+ @prompt.say(" Test Coverage: #{format_percentage_with_color(metrics[:test_coverage])}")
130
+ @prompt.say(" Code Quality: #{format_percentage_with_color(metrics[:code_quality])}")
131
+ @prompt.say(" PRD Task Progress: #{format_percentage_with_color(metrics[:prd_task_progress])}")
132
+ @prompt.say(" File Count: #{metrics[:file_count]}")
132
133
  end
133
134
 
134
135
  def display_status(status)
135
- puts " Overall Status: #{format_status(status)}"
136
+ @prompt.say(" Overall Status: #{format_status(status)}")
136
137
  end
137
138
 
138
139
  def format_status(status)
@@ -156,7 +157,7 @@ module Aidp
156
157
  arrow = trend_arrow(trend_data[:direction])
157
158
  change = format_change(trend_data[:change], trend_data[:change_percent])
158
159
 
159
- puts " #{metric_name}: #{arrow} #{change}"
160
+ @prompt.say(" #{metric_name}: #{arrow} #{change}")
160
161
  end
161
162
  end
162
163
 
@@ -185,7 +186,7 @@ module Aidp
185
186
  :red
186
187
  end
187
188
 
188
- puts " Quality Score: #{@pastel.send(color, "#{score.round(2)}%")}"
189
+ @prompt.say(" Quality Score: #{@pastel.send(color, "#{score.round(2)}%")}")
189
190
  end
190
191
 
191
192
  def format_percentage(value)
@@ -29,6 +29,7 @@ module Aidp
29
29
  @config = config
30
30
  @options = options
31
31
  @prompt = options[:prompt] || TTY::Prompt.new
32
+ @async_runner_class = options[:async_runner_class] || AsyncWorkLoopRunner
32
33
  @async_runner = nil
33
34
  @repl_macros = ReplMacros.new
34
35
  @output_display_thread = nil
@@ -38,7 +39,7 @@ module Aidp
38
39
 
39
40
  # Start work loop and enter interactive REPL
40
41
  def start_work_loop(step_name, step_spec, context = {})
41
- @async_runner = AsyncWorkLoopRunner.new(
42
+ @async_runner = @async_runner_class.new(
42
43
  @project_dir,
43
44
  @provider_manager,
44
45
  @config,
@@ -17,20 +17,20 @@ module Aidp
17
17
 
18
18
  attr_reader :optimizer, :last_optimization_stats
19
19
 
20
- def initialize(project_dir, config: nil)
20
+ def initialize(project_dir, config: nil, optimizer: nil)
21
21
  @project_dir = project_dir
22
22
  @aidp_dir = File.join(project_dir, ".aidp")
23
23
  @prompt_path = File.join(@aidp_dir, PROMPT_FILENAME)
24
24
  @archive_dir = File.join(project_dir, ARCHIVE_DIR)
25
25
  @config = config
26
- @optimizer = nil
26
+ @optimizer = optimizer
27
27
  @last_optimization_stats = nil
28
28
 
29
29
  # Ensure .aidp directory exists
30
30
  FileUtils.mkdir_p(@aidp_dir)
31
31
 
32
- # Initialize optimizer if enabled
33
- if config&.respond_to?(:prompt_optimization_enabled?) && config.prompt_optimization_enabled?
32
+ # Initialize optimizer if enabled and not provided
33
+ if @optimizer.nil? && config&.respond_to?(:prompt_optimization_enabled?) && config.prompt_optimization_enabled?
34
34
  @optimizer = Aidp::PromptOptimization::Optimizer.new(
35
35
  project_dir: project_dir,
36
36
  config: config.prompt_optimization_config
@@ -8,6 +8,7 @@ require_relative "work_loop_unit_scheduler"
8
8
  require_relative "deterministic_unit"
9
9
  require_relative "agent_signal_parser"
10
10
  require_relative "../harness/test_runner"
11
+ require_relative "../errors"
11
12
 
12
13
  module Aidp
13
14
  module Execute
@@ -46,10 +47,11 @@ module Aidp
46
47
  @project_dir = project_dir
47
48
  @provider_manager = provider_manager
48
49
  @config = config
50
+ @prompt = options[:prompt] || TTY::Prompt.new
49
51
  @prompt_manager = PromptManager.new(project_dir, config: config)
50
52
  @test_runner = Aidp::Harness::TestRunner.new(project_dir, config)
51
53
  @checkpoint = Checkpoint.new(project_dir)
52
- @checkpoint_display = CheckpointDisplay.new
54
+ @checkpoint_display = CheckpointDisplay.new(prompt: @prompt)
53
55
  @guard_policy = GuardPolicy.new(project_dir, config.guards_config)
54
56
  @persistent_tasklist = PersistentTasklist.new(project_dir)
55
57
  @iteration_count = 0
@@ -62,7 +64,7 @@ module Aidp
62
64
 
63
65
  # Initialize thinking depth manager for intelligent model selection
64
66
  require_relative "../harness/thinking_depth_manager"
65
- @thinking_depth_manager = Aidp::Harness::ThinkingDepthManager.new(config)
67
+ @thinking_depth_manager = options[:thinking_depth_manager] || Aidp::Harness::ThinkingDepthManager.new(config)
66
68
  @consecutive_failures = 0
67
69
  @last_tier = nil
68
70
  end
@@ -160,6 +162,10 @@ module Aidp
160
162
  # Wrap agent call in exception handling for true fix-forward
161
163
  begin
162
164
  agent_result = apply_patch
165
+ rescue Aidp::Errors::ConfigurationError
166
+ # Configuration errors should crash immediately (crash-early principle)
167
+ # Re-raise without catching
168
+ raise
163
169
  rescue => e
164
170
  # Convert exception to error result for fix-forward handling
165
171
  Aidp.logger.error("work_loop", "Exception during agent call",
@@ -178,6 +184,27 @@ module Aidp
178
184
  next
179
185
  end
180
186
 
187
+ # Check for fatal configuration errors (crash early per LLM_STYLE_GUIDE)
188
+ if agent_result[:status] == "error" && agent_result[:message]&.include?("No model available")
189
+ tier = @thinking_depth_manager.current_tier
190
+ provider = @provider_manager.current_provider
191
+
192
+ error_msg = "No model configured for thinking tier '#{tier}'.\n\n" \
193
+ "Current provider: #{provider}\n" \
194
+ "Required tier: #{tier}\n\n" \
195
+ "To fix this, add a model to your aidp.yml:\n\n" \
196
+ "thinking_depth:\n" \
197
+ " tiers:\n" \
198
+ " #{tier}:\n" \
199
+ " models:\n" \
200
+ " - provider: #{provider}\n" \
201
+ " model: <model-name> # e.g., claude-3-5-sonnet-20241022\n\n" \
202
+ "Or run: aidp models list\n" \
203
+ "to see available models for your configured providers."
204
+
205
+ raise Aidp::Errors::ConfigurationError, error_msg
206
+ end
207
+
181
208
  # Process agent output for task filing signals
182
209
  process_task_filing(agent_result)
183
210
 
@@ -10,10 +10,10 @@ module Aidp
10
10
  class WorkflowSelector
11
11
  include Aidp::MessageDisplay
12
12
 
13
- def initialize(prompt: TTY::Prompt.new)
13
+ def initialize(prompt: TTY::Prompt.new, workflow_selector: nil)
14
14
  @user_input = {}
15
15
  @prompt = prompt
16
- @workflow_selector = Aidp::Workflows::Selector.new(prompt: @prompt)
16
+ @workflow_selector = workflow_selector || Aidp::Workflows::Selector.new(prompt: @prompt)
17
17
  end
18
18
 
19
19
  # Main entry point for interactive workflow selection
@@ -359,19 +359,19 @@ module Aidp
359
359
  def load_config_with_options(options)
360
360
  # Apply different loading strategies based on options
361
361
  if options[:mode]
362
- @loader.get_mode_config(options[:mode], options[:force_reload])
362
+ @loader.mode_config(options[:mode], options[:force_reload])
363
363
  elsif options[:environment]
364
- @loader.get_environment_config(options[:environment], options[:force_reload])
364
+ @loader.environment_config(options[:environment], options[:force_reload])
365
365
  elsif options[:step]
366
366
  @loader.get_step_config(options[:step], options[:force_reload])
367
367
  elsif options[:features]
368
- @loader.get_config_with_features(options[:features], options[:force_reload])
368
+ @loader.config_with_features(options[:features], options[:force_reload])
369
369
  elsif options[:user]
370
370
  @loader.get_user_config(options[:user], options[:force_reload])
371
371
  elsif options[:time_based]
372
- @loader.get_time_based_config(options[:force_reload])
372
+ @loader.time_based_config(options[:force_reload])
373
373
  elsif options[:overrides]
374
- @loader.get_config_with_overrides(options[:overrides])
374
+ @loader.config_with_overrides(options[:overrides])
375
375
  else
376
376
  @loader.load_config(options[:force_reload])
377
377
  end
@@ -383,12 +383,12 @@ module Aidp
383
383
 
384
384
  # Get default thinking tier
385
385
  def default_tier
386
- thinking_config[:default_tier] || "standard"
386
+ thinking_config[:default_tier] || default_thinking_config[:default_tier]
387
387
  end
388
388
 
389
389
  # Get maximum thinking tier
390
390
  def max_tier
391
- thinking_config[:max_tier] || "standard"
391
+ thinking_config[:max_tier] || default_thinking_config[:max_tier]
392
392
  end
393
393
 
394
394
  # Check if provider switching for tier is allowed
@@ -432,6 +432,36 @@ module Aidp
432
432
  thinking_overrides[key] || thinking_overrides[key.to_sym]
433
433
  end
434
434
 
435
+ # Get tiers configuration from user's config
436
+ def thinking_tiers_config
437
+ thinking_config[:tiers] || {}
438
+ end
439
+
440
+ # Get models configured for a specific tier
441
+ # Returns array of {provider:, model:} hashes
442
+ def models_for_tier(tier)
443
+ tier_config = thinking_tiers_config[tier] || thinking_tiers_config[tier.to_sym]
444
+ return [] unless tier_config
445
+
446
+ models = tier_config[:models] || tier_config["models"]
447
+ return [] unless models
448
+
449
+ # Normalize to array of hashes with symbol keys
450
+ Array(models).map do |model_entry|
451
+ if model_entry.is_a?(Hash)
452
+ {
453
+ provider: (model_entry[:provider] || model_entry["provider"]).to_s,
454
+ model: (model_entry[:model] || model_entry["model"]).to_s
455
+ }
456
+ end
457
+ end.compact
458
+ end
459
+
460
+ # Get all configured tiers
461
+ def configured_tiers
462
+ thinking_tiers_config.keys.map(&:to_s)
463
+ end
464
+
435
465
  # Get fallback configuration
436
466
  def fallback_config
437
467
  harness_config[:fallback] || default_fallback_config
@@ -6,6 +6,7 @@ require_relative "ui/job_monitor"
6
6
  require_relative "ui/workflow_controller"
7
7
  require_relative "ui/progress_display"
8
8
  require_relative "ui/status_widget"
9
+ require_relative "../errors"
9
10
 
10
11
  module Aidp
11
12
  module Harness
@@ -47,27 +48,31 @@ module Aidp
47
48
  @selected_steps = options[:selected_steps] || []
48
49
  @workflow_type = options[:workflow_type] || :default
49
50
 
50
- # Initialize enhanced TUI components
51
- @tui = UI::EnhancedTUI.new
52
- @workflow_selector = UI::EnhancedWorkflowSelector.new(@tui)
53
- @job_monitor = UI::JobMonitor.new
54
- @workflow_controller = UI::WorkflowController.new
55
- @progress_display = UI::ProgressDisplay.new
56
- @status_widget = UI::StatusWidget.new
51
+ # Initialize enhanced TUI components (with dependency injection)
52
+ @tui = options[:tui] || UI::EnhancedTUI.new
53
+ @workflow_selector = options[:workflow_selector] || UI::EnhancedWorkflowSelector.new(@tui)
54
+ @job_monitor = options[:job_monitor] || UI::JobMonitor.new
55
+ @workflow_controller = options[:workflow_controller] || UI::WorkflowController.new
56
+ @progress_display = options[:progress_display] || UI::ProgressDisplay.new
57
+ @status_widget = options[:status_widget] || UI::StatusWidget.new
57
58
 
58
- # Initialize other components
59
- @configuration = Configuration.new(project_dir)
60
- @state_manager = StateManager.new(project_dir, @mode)
61
- @provider_manager = ProviderManager.new(@configuration, prompt: @prompt)
59
+ # Initialize other components (with dependency injection)
60
+ @configuration = options[:configuration] || Configuration.new(project_dir)
61
+ @state_manager = options[:state_manager] || StateManager.new(project_dir, @mode)
62
+ @provider_manager = options[:provider_manager] || ProviderManager.new(@configuration, prompt: @prompt)
62
63
 
63
64
  # Use ZFC-enabled condition detector
64
65
  # ZfcConditionDetector will create its own ProviderFactory if needed
65
66
  # Falls back to legacy pattern matching when ZFC is disabled
66
- require_relative "zfc_condition_detector"
67
- @condition_detector = ZfcConditionDetector.new(@configuration)
67
+ if options[:condition_detector]
68
+ @condition_detector = options[:condition_detector]
69
+ else
70
+ require_relative "zfc_condition_detector"
71
+ @condition_detector = ZfcConditionDetector.new(@configuration)
72
+ end
68
73
 
69
- @error_handler = ErrorHandler.new(@provider_manager, @configuration)
70
- @completion_checker = CompletionChecker.new(@project_dir, @workflow_type)
74
+ @error_handler = options[:error_handler] || ErrorHandler.new(@provider_manager, @configuration)
75
+ @completion_checker = options[:completion_checker] || CompletionChecker.new(@project_dir, @workflow_type)
71
76
  end
72
77
 
73
78
  # Get current provider (delegate to provider manager)
@@ -158,6 +163,10 @@ module Aidp
158
163
  handle_completion_criteria_not_met(completion_status)
159
164
  end
160
165
  end
166
+ rescue Aidp::Errors::ConfigurationError
167
+ # Configuration errors should crash immediately (crash-early principle)
168
+ # Re-raise without catching
169
+ raise
161
170
  rescue => e
162
171
  @state = STATES[:error]
163
172
  # Single error message - don't duplicate
@@ -4,6 +4,7 @@ require "net/http"
4
4
  require_relative "../debug_mixin"
5
5
  require_relative "../concurrency"
6
6
  require_relative "../providers/error_taxonomy"
7
+ require_relative "../errors"
7
8
 
8
9
  module Aidp
9
10
  module Harness
@@ -107,6 +108,10 @@ module Aidp
107
108
  begin
108
109
  attempt += 1
109
110
  return yield
111
+ rescue Aidp::Errors::ConfigurationError
112
+ # Configuration errors should crash immediately (crash-early principle)
113
+ # Re-raise without catching
114
+ raise
110
115
  rescue => error
111
116
  current_provider = current_provider_safely
112
117
 
@@ -483,7 +488,7 @@ module Aidp
483
488
  }
484
489
  end
485
490
 
486
- def attempt_provider_switch(error_info, _recovery_plan)
491
+ def attempt_provider_switch(error_info, recovery_plan)
487
492
  new_provider = @provider_manager.switch_provider_for_error(
488
493
  error_info[:error_type],
489
494
  error_info[:context]
@@ -497,6 +502,18 @@ module Aidp
497
502
  reason: "Error recovery: #{error_info[:error_type]}"
498
503
  }
499
504
  else
505
+ # If this is an auth error and we have no fallback providers, crash
506
+ if recovery_plan[:crash_if_no_fallback]
507
+ error_msg = "All providers have failed authentication.\n\n" \
508
+ "Last provider: #{error_info[:provider]}\n" \
509
+ "Error: #{error_info[:error]&.message || error_info[:error]}\n\n" \
510
+ "Please check your API credentials for all configured providers.\n" \
511
+ "Run 'aidp config --interactive' to update credentials."
512
+
513
+ raise Aidp::Errors::ConfigurationError, error_msg
514
+ end
515
+
516
+ # For non-auth errors, return failure result
500
517
  {
501
518
  success: false,
502
519
  action: :provider_switch_failed,
@@ -680,12 +697,13 @@ module Aidp
680
697
  priority: :high
681
698
  }
682
699
  when :auth_expired
683
- # Attempt a provider switch so workflows can continue with alternate providers
684
- # while the user resolves credentials for the failing provider
700
+ # Try to switch to another provider. If no providers available, this will
701
+ # be detected in attempt_recovery and we'll crash (crash-early principle)
685
702
  {
686
703
  action: :switch_provider,
687
704
  reason: "Authentication expired – switching provider to continue",
688
- priority: :critical
705
+ priority: :critical,
706
+ crash_if_no_fallback: true
689
707
  }
690
708
  when :quota_exceeded
691
709
  {
@@ -725,10 +743,13 @@ module Aidp
725
743
  priority: :medium
726
744
  }
727
745
  when :authentication, :permission_denied
746
+ # Try to switch to another provider. If no providers available, this will
747
+ # be detected in attempt_recovery and we'll crash (crash-early principle)
728
748
  {
729
749
  action: :switch_provider,
730
750
  reason: "Authentication/permission issue – switching provider to continue",
731
- priority: :critical
751
+ priority: :critical,
752
+ crash_if_no_fallback: true
732
753
  }
733
754
  when :rate_limit
734
755
  {