aidp 0.9.6 → 0.11.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +194 -25
  3. data/lib/aidp/analyze/error_handler.rb +4 -2
  4. data/lib/aidp/{analysis → analyze}/kb_inspector.rb +93 -89
  5. data/lib/aidp/analyze/prioritizer.rb +3 -2
  6. data/lib/aidp/analyze/progress.rb +2 -1
  7. data/lib/aidp/analyze/ruby_maat_integration.rb +7 -3
  8. data/lib/aidp/analyze/runner.rb +73 -11
  9. data/lib/aidp/{analysis → analyze}/seams.rb +1 -1
  10. data/lib/aidp/analyze/steps.rb +10 -8
  11. data/lib/aidp/{analysis → analyze}/tree_sitter_grammar_loader.rb +11 -5
  12. data/lib/aidp/{analysis → analyze}/tree_sitter_scan.rb +21 -15
  13. data/lib/aidp/cli/checkpoint_command.rb +98 -0
  14. data/lib/aidp/cli/first_run_wizard.rb +83 -103
  15. data/lib/aidp/cli/jobs_command.rb +270 -36
  16. data/lib/aidp/cli/terminal_io.rb +3 -3
  17. data/lib/aidp/cli.rb +411 -69
  18. data/lib/aidp/config.rb +5 -8
  19. data/lib/aidp/debug_logger.rb +4 -4
  20. data/lib/aidp/debug_mixin.rb +11 -4
  21. data/lib/aidp/execute/checkpoint.rb +282 -0
  22. data/lib/aidp/execute/checkpoint_display.rb +221 -0
  23. data/lib/aidp/execute/progress.rb +2 -1
  24. data/lib/aidp/execute/prompt_manager.rb +62 -0
  25. data/lib/aidp/execute/runner.rb +67 -20
  26. data/lib/aidp/execute/steps.rb +36 -27
  27. data/lib/aidp/execute/work_loop_runner.rb +308 -0
  28. data/lib/aidp/execute/workflow_selector.rb +50 -26
  29. data/lib/aidp/harness/condition_detector.rb +4 -4
  30. data/lib/aidp/harness/config_schema.rb +40 -0
  31. data/lib/aidp/harness/config_validator.rb +3 -6
  32. data/lib/aidp/harness/configuration.rb +35 -1
  33. data/lib/aidp/harness/enhanced_runner.rb +25 -4
  34. data/lib/aidp/harness/error_handler.rb +103 -28
  35. data/lib/aidp/harness/provider_factory.rb +6 -1
  36. data/lib/aidp/harness/provider_manager.rb +273 -19
  37. data/lib/aidp/harness/runner.rb +14 -6
  38. data/lib/aidp/harness/simple_user_interface.rb +6 -4
  39. data/lib/aidp/harness/status_display.rb +118 -106
  40. data/lib/aidp/harness/test_runner.rb +83 -0
  41. data/lib/aidp/harness/ui/enhanced_tui.rb +7 -5
  42. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +22 -4
  43. data/lib/aidp/harness/ui/error_handler.rb +7 -2
  44. data/lib/aidp/harness/ui/frame_manager.rb +61 -39
  45. data/lib/aidp/harness/ui/job_monitor.rb +2 -0
  46. data/lib/aidp/harness/ui/navigation/main_menu.rb +27 -16
  47. data/lib/aidp/harness/ui/navigation/menu_item.rb +1 -0
  48. data/lib/aidp/harness/ui/navigation/menu_state.rb +1 -0
  49. data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
  50. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +2 -0
  51. data/lib/aidp/harness/ui/progress_display.rb +26 -7
  52. data/lib/aidp/harness/ui/question_collector.rb +2 -0
  53. data/lib/aidp/harness/ui/spinner_group.rb +2 -0
  54. data/lib/aidp/harness/ui/spinner_helper.rb +1 -1
  55. data/lib/aidp/harness/ui/status_manager.rb +4 -2
  56. data/lib/aidp/harness/ui/status_widget.rb +20 -9
  57. data/lib/aidp/harness/ui/workflow_controller.rb +27 -9
  58. data/lib/aidp/harness/user_interface.rb +338 -330
  59. data/lib/aidp/jobs/background_runner.rb +278 -0
  60. data/lib/aidp/message_display.rb +48 -0
  61. data/lib/aidp/provider_manager.rb +13 -7
  62. data/lib/aidp/providers/anthropic.rb +101 -18
  63. data/lib/aidp/providers/base.rb +51 -1
  64. data/lib/aidp/providers/codex.rb +248 -0
  65. data/lib/aidp/providers/cursor.rb +39 -48
  66. data/lib/aidp/providers/gemini.rb +26 -16
  67. data/lib/aidp/providers/github_copilot.rb +263 -0
  68. data/lib/aidp/providers/opencode.rb +38 -47
  69. data/lib/aidp/version.rb +1 -1
  70. data/lib/aidp/workflows/definitions.rb +357 -0
  71. data/lib/aidp/workflows/selector.rb +171 -0
  72. data/lib/aidp.rb +16 -4
  73. data/templates/planning/generate_llm_style_guide.md +119 -0
  74. metadata +43 -31
  75. data/lib/aidp/analyze/progress_visualizer.rb +0 -314
  76. /data/templates/{ANALYZE/02_ARCHITECTURE_ANALYSIS.md → analysis/analyze_architecture.md} +0 -0
  77. /data/templates/{ANALYZE/05_DOCUMENTATION_ANALYSIS.md → analysis/analyze_documentation.md} +0 -0
  78. /data/templates/{ANALYZE/04_FUNCTIONALITY_ANALYSIS.md → analysis/analyze_functionality.md} +0 -0
  79. /data/templates/{ANALYZE/01_REPOSITORY_ANALYSIS.md → analysis/analyze_repository.md} +0 -0
  80. /data/templates/{ANALYZE/06_STATIC_ANALYSIS.md → analysis/analyze_static_code.md} +0 -0
  81. /data/templates/{ANALYZE/03_TEST_ANALYSIS.md → analysis/analyze_tests.md} +0 -0
  82. /data/templates/{ANALYZE/07_REFACTORING_RECOMMENDATIONS.md → analysis/recommend_refactoring.md} +0 -0
  83. /data/templates/{ANALYZE/06a_tree_sitter_scan.md → analysis/scan_with_tree_sitter.md} +0 -0
  84. /data/templates/{EXECUTE/11_STATIC_ANALYSIS.md → implementation/configure_static_analysis.md} +0 -0
  85. /data/templates/{EXECUTE/14_DOCS_PORTAL.md → implementation/create_documentation_portal.md} +0 -0
  86. /data/templates/{EXECUTE/10_IMPLEMENTATION_AGENT.md → implementation/implement_features.md} +0 -0
  87. /data/templates/{EXECUTE/13_DELIVERY_ROLLOUT.md → implementation/plan_delivery.md} +0 -0
  88. /data/templates/{EXECUTE/15_POST_RELEASE.md → implementation/review_post_release.md} +0 -0
  89. /data/templates/{EXECUTE/09_SCAFFOLDING_DEVEX.md → implementation/setup_scaffolding.md} +0 -0
  90. /data/templates/{EXECUTE/02A_ARCH_GATE_QUESTIONS.md → planning/ask_architecture_questions.md} +0 -0
  91. /data/templates/{EXECUTE/00_PRD.md → planning/create_prd.md} +0 -0
  92. /data/templates/{EXECUTE/08_TASKS.md → planning/create_tasks.md} +0 -0
  93. /data/templates/{EXECUTE/04_DOMAIN_DECOMPOSITION.md → planning/decompose_domain.md} +0 -0
  94. /data/templates/{EXECUTE/01_NFRS.md → planning/define_nfrs.md} +0 -0
  95. /data/templates/{EXECUTE/05_CONTRACTS.md → planning/design_apis.md} +0 -0
  96. /data/templates/{EXECUTE/02_ARCHITECTURE.md → planning/design_architecture.md} +0 -0
  97. /data/templates/{EXECUTE/06_THREAT_MODEL.md → planning/design_data_model.md} +0 -0
  98. /data/templates/{EXECUTE/03_ADR_FACTORY.md → planning/generate_adrs.md} +0 -0
  99. /data/templates/{EXECUTE/12_OBSERVABILITY_SLOS.md → planning/plan_observability.md} +0 -0
  100. /data/templates/{EXECUTE/07_TEST_PLAN.md → planning/plan_testing.md} +0 -0
@@ -1,32 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "tty-prompt"
4
+ require_relative "../workflows/definitions"
5
+ require_relative "../workflows/selector"
4
6
 
5
7
  module Aidp
6
8
  module Execute
7
9
  # Handles interactive workflow selection and project setup
8
10
  class WorkflowSelector
11
+ include Aidp::MessageDisplay
12
+
9
13
  def initialize(prompt: TTY::Prompt.new)
10
14
  @user_input = {}
11
15
  @prompt = prompt
16
+ @workflow_selector = Aidp::Workflows::Selector.new(prompt: @prompt)
12
17
  end
13
18
 
14
19
  # Main entry point for interactive workflow selection
15
- def select_workflow(harness_mode: false)
20
+ def select_workflow(harness_mode: false, use_new_selector: true, mode: nil)
16
21
  if harness_mode
17
22
  # In harness mode, use default values to avoid blocking
18
23
  select_workflow_with_defaults
24
+ elsif use_new_selector
25
+ # Use new unified workflow selector (default as of Issue #79)
26
+ select_workflow_with_new_selector(mode)
19
27
  else
20
- # Interactive mode for standalone usage
28
+ # Legacy interactive mode for backward compatibility
21
29
  select_workflow_interactive
22
30
  end
23
31
  end
24
32
 
25
33
  private
26
34
 
35
+ def select_workflow_with_new_selector(mode = nil)
36
+ # Step 1: Collect project information (still useful for all workflows)
37
+ collect_project_info
38
+
39
+ # Step 2: Use new workflow selector, defaulting to execute mode if not specified
40
+ workflow_mode = mode || :execute
41
+ result = @workflow_selector.select_workflow(workflow_mode)
42
+
43
+ {
44
+ workflow_type: result[:workflow_key],
45
+ steps: result[:steps],
46
+ user_input: @user_input,
47
+ workflow: result[:workflow]
48
+ }
49
+ end
50
+
27
51
  def select_workflow_interactive
28
- puts "\n🚀 Welcome to AI Dev Pipeline!"
29
- puts "Let's set up your development workflow.\n\n"
52
+ display_message("\n🚀 Welcome to AI Dev Pipeline!", type: :highlight)
53
+ display_message("Let's set up your development workflow.\n\n")
30
54
 
31
55
  # Step 1: Collect project information
32
56
  collect_project_info
@@ -45,7 +69,7 @@ module Aidp
45
69
  end
46
70
 
47
71
  def select_workflow_with_defaults
48
- puts "\n🚀 Starting harness with default workflow configuration..."
72
+ display_message("\n🚀 Starting harness with default workflow configuration...", type: :highlight)
49
73
 
50
74
  # Use default project information
51
75
  @user_input = {
@@ -71,7 +95,7 @@ module Aidp
71
95
  private
72
96
 
73
97
  def collect_project_info
74
- puts "📋 First, tell us about your project:\n"
98
+ display_message("📋 First, tell us about your project:\n", type: :highlight)
75
99
 
76
100
  @user_input[:project_description] = prompt_required(
77
101
  "What do you want to build? (Be specific about features and goals)"
@@ -91,17 +115,17 @@ module Aidp
91
115
  end
92
116
 
93
117
  def choose_workflow_type
94
- puts "\n🎯 Choose your development approach:\n"
95
- puts "1. 🔬 Exploration/Experiment - Quick prototype or proof of concept"
96
- puts " • Fast iteration, minimal documentation"
97
- puts " • Focus on core functionality and validation"
98
- puts " • Steps: PRD → Tasks → Implementation"
99
- puts ""
100
- puts "2. 🏗️ Full Development - Production-ready feature or system"
101
- puts " • Comprehensive planning and documentation"
102
- puts " • You can customize which steps to include"
103
- puts " • Full enterprise workflow available"
104
- puts ""
118
+ display_message("\n🎯 Choose your development approach:\n", type: :highlight)
119
+ display_message("1. 🔬 Exploration/Experiment - Quick prototype or proof of concept")
120
+ display_message(" • Fast iteration, minimal documentation", type: :muted)
121
+ display_message(" • Focus on core functionality and validation", type: :muted)
122
+ display_message(" • Steps: PRD → Tasks → Implementation", type: :muted)
123
+ display_message("")
124
+ display_message("2. 🏗️ Full Development - Production-ready feature or system")
125
+ display_message(" • Comprehensive planning and documentation", type: :muted)
126
+ display_message(" • You can customize which steps to include", type: :muted)
127
+ display_message(" • Full enterprise workflow available", type: :muted)
128
+ display_message("")
105
129
 
106
130
  choice = prompt_choice("Which approach fits your project?", ["1", "2", "exploration", "full"])
107
131
 
@@ -111,7 +135,7 @@ module Aidp
111
135
  when "2", "full"
112
136
  :full
113
137
  else
114
- puts "Invalid choice. Defaulting to exploration workflow."
138
+ display_message("Invalid choice. Defaulting to exploration workflow.", type: :warning)
115
139
  :exploration
116
140
  end
117
141
  end
@@ -129,16 +153,16 @@ module Aidp
129
153
 
130
154
  def exploration_workflow_steps
131
155
  [
132
- "00_PRD", # Generate PRD from user input (no manual gate)
156
+ "00_PRD", # Generate PRD from user input (no manual gate)
133
157
  "10_TESTING_STRATEGY", # Ensure we have tests
134
- "11_STATIC_ANALYSIS", # Code quality
135
- "16_IMPLEMENTATION" # Special step for actual development work
158
+ "11_STATIC_ANALYSIS", # Code quality
159
+ "16_IMPLEMENTATION" # Special step for actual development work
136
160
  ]
137
161
  end
138
162
 
139
163
  def full_workflow_steps
140
- puts "\n🛠️ Customize your full development workflow:\n"
141
- puts "Select the steps you want to include (enter numbers separated by commas):\n"
164
+ display_message("\n🛠️ Customize your full development workflow:\n", type: :highlight)
165
+ display_message("Select the steps you want to include (enter numbers separated by commas):\n")
142
166
 
143
167
  available_steps = {
144
168
  "1" => "00_PRD - Product Requirements Document",
@@ -155,8 +179,8 @@ module Aidp
155
179
  "12" => "13_DELIVERY_ROLLOUT - Deployment Planning"
156
180
  }
157
181
 
158
- available_steps.each { |num, desc| puts " #{num}. #{desc}" }
159
- puts ""
182
+ available_steps.each { |num, desc| display_message(" #{num}. #{desc}") }
183
+ display_message("")
160
184
 
161
185
  selected = prompt_required("Enter step numbers (e.g., 1,3,5,9,10): ")
162
186
  selected_numbers = selected.split(",").map(&:strip).map(&:to_i)
@@ -193,7 +217,7 @@ module Aidp
193
217
  input = @prompt.ask("#{question}:")
194
218
 
195
219
  if input.nil? || input.strip.empty?
196
- puts "❌ This field is required. Please provide an answer."
220
+ display_message("❌ This field is required. Please provide an answer.", type: :error)
197
221
  next
198
222
  end
199
223
 
@@ -1310,13 +1310,13 @@ module Aidp
1310
1310
  # Get timeout duration for operation type
1311
1311
  def get_timeout_duration(operation_type, configuration = nil)
1312
1312
  default_timeouts = {
1313
- analyze: 300, # 5 minutes
1314
- execute: 600, # 10 minutes
1313
+ analyze: 300, # 5 minutes
1314
+ execute: 600, # 10 minutes
1315
1315
  provider_call: 120, # 2 minutes
1316
1316
  file_operation: 30, # 30 seconds
1317
1317
  network_request: 60, # 1 minute
1318
- user_input: 300, # 5 minutes
1319
- default: 120 # 2 minutes
1318
+ user_input: 300, # 5 minutes
1319
+ default: 120 # 2 minutes
1320
1320
  }
1321
1321
 
1322
1322
  # Get timeout from configuration if available
@@ -366,6 +366,46 @@ module Aidp
366
366
  enum: ["provider_model", "provider", "model", "none"]
367
367
  }
368
368
  }
369
+ },
370
+ work_loop: {
371
+ type: :hash,
372
+ required: false,
373
+ default: {
374
+ enabled: true,
375
+ max_iterations: 50,
376
+ test_commands: [],
377
+ lint_commands: []
378
+ },
379
+ properties: {
380
+ enabled: {
381
+ type: :boolean,
382
+ required: false,
383
+ default: true
384
+ },
385
+ max_iterations: {
386
+ type: :integer,
387
+ required: false,
388
+ default: 50,
389
+ min: 1,
390
+ max: 200
391
+ },
392
+ test_commands: {
393
+ type: :array,
394
+ required: false,
395
+ default: [],
396
+ items: {
397
+ type: :string
398
+ }
399
+ },
400
+ lint_commands: {
401
+ type: :array,
402
+ required: false,
403
+ default: [],
404
+ items: {
405
+ type: :string
406
+ }
407
+ }
408
+ }
369
409
  }
370
410
  }
371
411
  },
@@ -76,8 +76,9 @@ module Aidp
76
76
  return false if config_exists?
77
77
 
78
78
  example_config = ConfigSchema.generate_example
79
- config_path = File.join(@project_dir, "aidp.yml")
79
+ config_path = File.join(@project_dir, ".aidp", "aidp.yml")
80
80
 
81
+ FileUtils.mkdir_p(File.dirname(config_path))
81
82
  File.write(config_path, YAML.dump(example_config))
82
83
  true
83
84
  end
@@ -243,14 +244,10 @@ module Aidp
243
244
  private
244
245
 
245
246
  def find_config_file
246
- # Try new aidp.yml format first, then fall back to .aidp.yml
247
- config_file = File.join(@project_dir, "aidp.yml")
248
- legacy_config_file = File.join(@project_dir, ".aidp.yml")
247
+ config_file = File.join(@project_dir, ".aidp", "aidp.yml")
249
248
 
250
249
  if File.exist?(config_file)
251
250
  config_file
252
- elsif File.exist?(legacy_config_file)
253
- legacy_config_file
254
251
  end
255
252
  end
256
253
 
@@ -144,6 +144,31 @@ module Aidp
144
144
  harness_config[:session] || get_default_session_config
145
145
  end
146
146
 
147
+ # Get work loop configuration
148
+ def work_loop_config
149
+ harness_config[:work_loop] || get_default_work_loop_config
150
+ end
151
+
152
+ # Check if work loops are enabled
153
+ def work_loop_enabled?
154
+ work_loop_config[:enabled]
155
+ end
156
+
157
+ # Get maximum iterations for work loops
158
+ def work_loop_max_iterations
159
+ work_loop_config[:max_iterations]
160
+ end
161
+
162
+ # Get test commands
163
+ def test_commands
164
+ work_loop_config[:test_commands] || []
165
+ end
166
+
167
+ # Get lint commands
168
+ def lint_commands
169
+ work_loop_config[:lint_commands] || []
170
+ end
171
+
147
172
  # Get provider priority
148
173
  def provider_priority(provider_name)
149
174
  provider_config(provider_name)[:priority] || 0
@@ -186,7 +211,7 @@ module Aidp
186
211
 
187
212
  # Get configuration path
188
213
  def config_path
189
- File.join(@project_dir, "aidp.yml")
214
+ File.join(@project_dir, ".aidp", "aidp.yml")
190
215
  end
191
216
 
192
217
  # Get logging configuration
@@ -416,6 +441,15 @@ module Aidp
416
441
  }
417
442
  end
418
443
 
444
+ def get_default_work_loop_config
445
+ {
446
+ enabled: true,
447
+ max_iterations: 50,
448
+ test_commands: [],
449
+ lint_commands: []
450
+ }
451
+ end
452
+
419
453
  def get_default_logging_config
420
454
  {
421
455
  log_level: :info,
@@ -31,7 +31,7 @@ module Aidp
31
31
  @current_step = nil
32
32
  @current_provider = nil
33
33
  @user_input = options[:user_input] || {}
34
- @user_input = {} if @user_input.nil? # Ensure it's never nil
34
+ @user_input = {} if @user_input.nil? # Ensure it's never nil
35
35
  @execution_log = []
36
36
 
37
37
  # Store workflow configuration
@@ -50,11 +50,32 @@ module Aidp
50
50
  @configuration = Configuration.new(project_dir)
51
51
  @state_manager = StateManager.new(project_dir, @mode)
52
52
  @condition_detector = ConditionDetector.new
53
- @provider_manager = ProviderManager.new(@configuration)
53
+ @provider_manager = ProviderManager.new(@configuration, prompt: TTY::Prompt.new)
54
54
  @error_handler = ErrorHandler.new(@provider_manager, @configuration)
55
55
  @completion_checker = CompletionChecker.new(@project_dir, @workflow_type)
56
56
  end
57
57
 
58
+ # Get current provider (delegate to provider manager)
59
+ def current_provider
60
+ @current_provider || @provider_manager&.current_provider || "unknown"
61
+ end
62
+
63
+ # Get current step
64
+ attr_reader :current_step
65
+
66
+ # Get user input
67
+ def user_input
68
+ @user_input || {}
69
+ end
70
+
71
+ # Get execution log
72
+ def execution_log
73
+ @execution_log || []
74
+ end
75
+
76
+ # Get provider manager
77
+ attr_reader :provider_manager
78
+
58
79
  # Main execution method with enhanced TUI
59
80
  def run
60
81
  @state = STATES[:running]
@@ -312,9 +333,9 @@ module Aidp
312
333
  def get_mode_runner
313
334
  case @mode
314
335
  when :analyze
315
- Aidp::Analyze::Runner.new(@project_dir, self)
336
+ Aidp::Analyze::Runner.new(@project_dir, self, prompt: TTY::Prompt.new)
316
337
  when :execute
317
- Aidp::Execute::Runner.new(@project_dir, self)
338
+ Aidp::Execute::Runner.new(@project_dir, self, prompt: TTY::Prompt.new)
318
339
  else
319
340
  raise ArgumentError, "Unsupported mode: #{@mode}"
320
341
  end
@@ -69,6 +69,17 @@ module Aidp
69
69
  error_type: error_info[:error_type],
70
70
  reason: "Retry not applicable or exhausted"
71
71
  })
72
+ if [:authentication, :permission_denied].include?(error_info[:error_type].to_sym)
73
+ # Mark provider unhealthy to avoid immediate re-selection
74
+ begin
75
+ if @provider_manager.respond_to?(:mark_provider_auth_failure)
76
+ @provider_manager.mark_provider_auth_failure(error_info[:provider])
77
+ debug_log("🔐 Marked provider #{error_info[:provider]} unhealthy due to auth error", level: :warn)
78
+ end
79
+ rescue => e
80
+ debug_log("⚠️ Failed to mark provider unhealthy after auth error", level: :warn, data: {error: e.message})
81
+ end
82
+ end
72
83
  attempt_recovery(error_info, context)
73
84
 
74
85
  end
@@ -76,35 +87,75 @@ module Aidp
76
87
 
77
88
  # Execute a block with retry logic
78
89
  def execute_with_retry(&block)
79
- max_attempts = @configuration.max_retries + 1
80
- attempt = 0
81
-
82
- begin
83
- attempt += 1
84
- yield
85
- rescue => error
86
- if attempt < max_attempts
87
- error_info = {
88
- error: error,
89
- provider: @provider_manager.current_provider,
90
- model: @provider_manager.current_model,
91
- error_type: @error_classifier.classify_error(error)
92
- }
90
+ providers_tried = []
91
+
92
+ loop do
93
+ max_attempts = @configuration.max_retries + 1
94
+ attempt = 0
95
+
96
+ begin
97
+ attempt += 1
98
+ return yield
99
+ rescue => error
100
+ current_provider = get_current_provider_safely
101
+
102
+ if attempt < max_attempts
103
+ error_info = {
104
+ error: error,
105
+ provider: current_provider,
106
+ model: get_current_model_safely,
107
+ error_type: @error_classifier.classify_error(error)
108
+ }
109
+
110
+ strategy = get_retry_strategy(error_info[:error_type])
111
+ if should_retry?(error_info, strategy)
112
+ delay = @backoff_calculator.calculate_delay(attempt, strategy[:backoff_strategy] || :exponential, 1, 10)
113
+ debug_log("🔁 Retry attempt #{attempt} for #{current_provider}", level: :info, data: {delay: delay, error_type: error_info[:error_type]})
114
+ sleep(delay) if delay > 0
115
+ retry
116
+ end
117
+ end
93
118
 
94
- strategy = get_retry_strategy(error_info[:error_type])
95
- if should_retry?(error_info, strategy)
96
- delay = @backoff_calculator.calculate_delay(attempt, strategy[:backoff_strategy] || :exponential, 1, 10)
97
- # Use regular sleep for now (async not needed in this context)
98
- sleep(delay)
99
- retry
119
+ # Provider exhausted – attempt recovery (may switch provider)
120
+ debug_log("🚫 Exhausted retries for provider, attempting recovery", level: :warn, data: {provider: current_provider, attempt: attempt, max_attempts: max_attempts})
121
+ handle_error(error, {
122
+ provider: current_provider,
123
+ model: get_current_model_safely,
124
+ exhausted_retries: true
125
+ })
126
+
127
+ new_provider = get_current_provider_safely
128
+ if new_provider != current_provider && !providers_tried.include?(new_provider)
129
+ providers_tried << current_provider
130
+ # Reset retry counts for the new provider
131
+ begin
132
+ reset_retry_counts(new_provider)
133
+ rescue => e
134
+ debug_log("⚠️ Failed to reset retry counts for new provider", level: :warn, data: {error: e.message})
135
+ end
136
+ debug_log("🔀 Switched provider after failure – re-executing block", level: :info, data: {from: current_provider, to: new_provider})
137
+ # Start retry loop fresh for new provider
138
+ next
100
139
  end
101
- end
102
140
 
103
- # If we get here, all retries failed
104
- handle_error(error, {
105
- provider: @provider_manager.current_provider,
106
- model: @provider_manager.current_model
107
- })
141
+ # No new provider (or already tried) – return structured failure
142
+ debug_log("❌ No fallback provider available or all tried", level: :error, data: {providers_tried: providers_tried})
143
+ begin
144
+ if @provider_manager.respond_to?(:mark_provider_failure_exhausted)
145
+ @provider_manager.mark_provider_failure_exhausted(current_provider)
146
+ debug_log("🛑 Marked provider #{current_provider} unhealthy due to exhausted retries", level: :warn)
147
+ end
148
+ rescue => e
149
+ debug_log("⚠️ Failed to mark provider failure-exhausted", level: :warn, data: {error: e.message})
150
+ end
151
+ return {
152
+ status: "failed",
153
+ error: error,
154
+ message: error.message,
155
+ provider: current_provider,
156
+ providers_tried: providers_tried.dup
157
+ }
158
+ end
108
159
  end
109
160
  end
110
161
 
@@ -579,9 +630,12 @@ module Aidp
579
630
  priority: :high
580
631
  }
581
632
  when :authentication, :permission_denied
633
+ # Previously we escalated immediately. Instead, attempt a provider switch
634
+ # so workflows can continue with alternate providers (e.g., Gemini, Cursor)
635
+ # while the user resolves credentials for the failing provider.
582
636
  {
583
- action: :escalate,
584
- reason: "Authentication or permission issue requires manual intervention",
637
+ action: :switch_provider,
638
+ reason: "Authentication/permission issue switching provider to continue",
585
639
  priority: :critical
586
640
  }
587
641
  when :timeout
@@ -611,6 +665,27 @@ module Aidp
611
665
  end
612
666
  end
613
667
  end
668
+
669
+ # Safe access to provider manager methods that may not exist
670
+ def get_current_provider_safely
671
+ return "unknown" unless @provider_manager
672
+ return "unknown" unless @provider_manager.respond_to?(:current_provider)
673
+
674
+ @provider_manager.current_provider || "unknown"
675
+ rescue => e
676
+ debug_log("⚠️ Failed to get current provider", level: :warn, data: {error: e.message})
677
+ "unknown"
678
+ end
679
+
680
+ def get_current_model_safely
681
+ return "unknown" unless @provider_manager
682
+ return "unknown" unless @provider_manager.respond_to?(:current_model)
683
+
684
+ @provider_manager.current_model || "unknown"
685
+ rescue => e
686
+ debug_log("⚠️ Failed to get current model", level: :warn, data: {error: e.message})
687
+ "unknown"
688
+ end
614
689
  end
615
690
  end
616
691
  end
@@ -7,6 +7,8 @@ require_relative "../providers/anthropic"
7
7
  require_relative "../providers/gemini"
8
8
  require_relative "../providers/macos_ui"
9
9
  require_relative "../providers/opencode"
10
+ require_relative "../providers/github_copilot"
11
+ require_relative "../providers/codex"
10
12
 
11
13
  module Aidp
12
14
  module Harness
@@ -15,9 +17,12 @@ module Aidp
15
17
  PROVIDER_CLASSES = {
16
18
  "cursor" => Aidp::Providers::Cursor,
17
19
  "anthropic" => Aidp::Providers::Anthropic,
20
+ "claude" => Aidp::Providers::Anthropic,
18
21
  "gemini" => Aidp::Providers::Gemini,
19
22
  "macos" => Aidp::Providers::MacOSUI,
20
- "opencode" => Aidp::Providers::Opencode
23
+ "opencode" => Aidp::Providers::Opencode,
24
+ "github_copilot" => Aidp::Providers::GithubCopilot,
25
+ "codex" => Aidp::Providers::Codex
21
26
  }.freeze
22
27
 
23
28
  def initialize(config_manager = nil)