aidp 0.10.0 → 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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +194 -25
  3. data/lib/aidp/analyze/kb_inspector.rb +2 -15
  4. data/lib/aidp/analyze/progress.rb +2 -1
  5. data/lib/aidp/analyze/ruby_maat_integration.rb +2 -15
  6. data/lib/aidp/analyze/runner.rb +64 -20
  7. data/lib/aidp/analyze/steps.rb +10 -8
  8. data/lib/aidp/analyze/tree_sitter_grammar_loader.rb +2 -13
  9. data/lib/aidp/analyze/tree_sitter_scan.rb +2 -13
  10. data/lib/aidp/cli/checkpoint_command.rb +98 -0
  11. data/lib/aidp/cli/first_run_wizard.rb +65 -94
  12. data/lib/aidp/cli/jobs_command.rb +249 -34
  13. data/lib/aidp/cli.rb +312 -38
  14. data/lib/aidp/config.rb +5 -8
  15. data/lib/aidp/debug_logger.rb +4 -4
  16. data/lib/aidp/debug_mixin.rb +11 -4
  17. data/lib/aidp/execute/checkpoint.rb +282 -0
  18. data/lib/aidp/execute/checkpoint_display.rb +221 -0
  19. data/lib/aidp/execute/progress.rb +2 -1
  20. data/lib/aidp/execute/prompt_manager.rb +62 -0
  21. data/lib/aidp/execute/runner.rb +53 -24
  22. data/lib/aidp/execute/steps.rb +36 -27
  23. data/lib/aidp/execute/work_loop_runner.rb +308 -0
  24. data/lib/aidp/execute/workflow_selector.rb +26 -17
  25. data/lib/aidp/harness/condition_detector.rb +4 -4
  26. data/lib/aidp/harness/config_schema.rb +40 -0
  27. data/lib/aidp/harness/config_validator.rb +3 -6
  28. data/lib/aidp/harness/configuration.rb +35 -1
  29. data/lib/aidp/harness/enhanced_runner.rb +22 -1
  30. data/lib/aidp/harness/error_handler.rb +103 -28
  31. data/lib/aidp/harness/provider_factory.rb +4 -1
  32. data/lib/aidp/harness/provider_manager.rb +250 -15
  33. data/lib/aidp/harness/runner.rb +3 -14
  34. data/lib/aidp/harness/simple_user_interface.rb +2 -15
  35. data/lib/aidp/harness/status_display.rb +12 -17
  36. data/lib/aidp/harness/test_runner.rb +83 -0
  37. data/lib/aidp/harness/ui/enhanced_tui.rb +2 -0
  38. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +22 -4
  39. data/lib/aidp/harness/ui/error_handler.rb +4 -0
  40. data/lib/aidp/harness/ui/frame_manager.rb +10 -8
  41. data/lib/aidp/harness/ui/job_monitor.rb +2 -0
  42. data/lib/aidp/harness/ui/navigation/main_menu.rb +4 -2
  43. data/lib/aidp/harness/ui/navigation/menu_item.rb +1 -0
  44. data/lib/aidp/harness/ui/navigation/menu_state.rb +1 -0
  45. data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
  46. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +2 -0
  47. data/lib/aidp/harness/ui/progress_display.rb +8 -12
  48. data/lib/aidp/harness/ui/question_collector.rb +2 -0
  49. data/lib/aidp/harness/ui/spinner_group.rb +2 -0
  50. data/lib/aidp/harness/ui/spinner_helper.rb +1 -1
  51. data/lib/aidp/harness/ui/status_manager.rb +4 -2
  52. data/lib/aidp/harness/ui/status_widget.rb +3 -1
  53. data/lib/aidp/harness/ui/workflow_controller.rb +2 -0
  54. data/lib/aidp/harness/user_interface.rb +12 -17
  55. data/lib/aidp/jobs/background_runner.rb +278 -0
  56. data/lib/aidp/message_display.rb +48 -0
  57. data/lib/aidp/provider_manager.rb +3 -1
  58. data/lib/aidp/providers/anthropic.rb +100 -17
  59. data/lib/aidp/providers/base.rb +42 -11
  60. data/lib/aidp/providers/codex.rb +248 -0
  61. data/lib/aidp/providers/cursor.rb +35 -42
  62. data/lib/aidp/providers/gemini.rb +25 -15
  63. data/lib/aidp/providers/github_copilot.rb +41 -42
  64. data/lib/aidp/providers/opencode.rb +34 -41
  65. data/lib/aidp/version.rb +1 -1
  66. data/lib/aidp/workflows/definitions.rb +357 -0
  67. data/lib/aidp/workflows/selector.rb +171 -0
  68. data/lib/aidp.rb +12 -0
  69. data/templates/planning/generate_llm_style_guide.md +119 -0
  70. metadata +38 -26
  71. /data/templates/{ANALYZE/02_ARCHITECTURE_ANALYSIS.md → analysis/analyze_architecture.md} +0 -0
  72. /data/templates/{ANALYZE/05_DOCUMENTATION_ANALYSIS.md → analysis/analyze_documentation.md} +0 -0
  73. /data/templates/{ANALYZE/04_FUNCTIONALITY_ANALYSIS.md → analysis/analyze_functionality.md} +0 -0
  74. /data/templates/{ANALYZE/01_REPOSITORY_ANALYSIS.md → analysis/analyze_repository.md} +0 -0
  75. /data/templates/{ANALYZE/06_STATIC_ANALYSIS.md → analysis/analyze_static_code.md} +0 -0
  76. /data/templates/{ANALYZE/03_TEST_ANALYSIS.md → analysis/analyze_tests.md} +0 -0
  77. /data/templates/{ANALYZE/07_REFACTORING_RECOMMENDATIONS.md → analysis/recommend_refactoring.md} +0 -0
  78. /data/templates/{ANALYZE/06a_tree_sitter_scan.md → analysis/scan_with_tree_sitter.md} +0 -0
  79. /data/templates/{EXECUTE/11_STATIC_ANALYSIS.md → implementation/configure_static_analysis.md} +0 -0
  80. /data/templates/{EXECUTE/14_DOCS_PORTAL.md → implementation/create_documentation_portal.md} +0 -0
  81. /data/templates/{EXECUTE/10_IMPLEMENTATION_AGENT.md → implementation/implement_features.md} +0 -0
  82. /data/templates/{EXECUTE/13_DELIVERY_ROLLOUT.md → implementation/plan_delivery.md} +0 -0
  83. /data/templates/{EXECUTE/15_POST_RELEASE.md → implementation/review_post_release.md} +0 -0
  84. /data/templates/{EXECUTE/09_SCAFFOLDING_DEVEX.md → implementation/setup_scaffolding.md} +0 -0
  85. /data/templates/{EXECUTE/02A_ARCH_GATE_QUESTIONS.md → planning/ask_architecture_questions.md} +0 -0
  86. /data/templates/{EXECUTE/00_PRD.md → planning/create_prd.md} +0 -0
  87. /data/templates/{EXECUTE/08_TASKS.md → planning/create_tasks.md} +0 -0
  88. /data/templates/{EXECUTE/04_DOMAIN_DECOMPOSITION.md → planning/decompose_domain.md} +0 -0
  89. /data/templates/{EXECUTE/01_NFRS.md → planning/define_nfrs.md} +0 -0
  90. /data/templates/{EXECUTE/05_CONTRACTS.md → planning/design_apis.md} +0 -0
  91. /data/templates/{EXECUTE/02_ARCHITECTURE.md → planning/design_architecture.md} +0 -0
  92. /data/templates/{EXECUTE/06_THREAT_MODEL.md → planning/design_data_model.md} +0 -0
  93. /data/templates/{EXECUTE/03_ADR_FACTORY.md → planning/generate_adrs.md} +0 -0
  94. /data/templates/{EXECUTE/12_OBSERVABILITY_SLOS.md → planning/plan_observability.md} +0 -0
  95. /data/templates/{EXECUTE/07_TEST_PLAN.md → planning/plan_testing.md} +0 -0
@@ -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
@@ -55,6 +55,27 @@ module Aidp
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]
@@ -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
@@ -8,6 +8,7 @@ require_relative "../providers/gemini"
8
8
  require_relative "../providers/macos_ui"
9
9
  require_relative "../providers/opencode"
10
10
  require_relative "../providers/github_copilot"
11
+ require_relative "../providers/codex"
11
12
 
12
13
  module Aidp
13
14
  module Harness
@@ -16,10 +17,12 @@ module Aidp
16
17
  PROVIDER_CLASSES = {
17
18
  "cursor" => Aidp::Providers::Cursor,
18
19
  "anthropic" => Aidp::Providers::Anthropic,
20
+ "claude" => Aidp::Providers::Anthropic,
19
21
  "gemini" => Aidp::Providers::Gemini,
20
22
  "macos" => Aidp::Providers::MacOSUI,
21
23
  "opencode" => Aidp::Providers::Opencode,
22
- "github_copilot" => Aidp::Providers::GithubCopilot
24
+ "github_copilot" => Aidp::Providers::GithubCopilot,
25
+ "codex" => Aidp::Providers::Codex
23
26
  }.freeze
24
27
 
25
28
  def initialize(config_manager = nil)
@@ -7,6 +7,8 @@ module Aidp
7
7
  module Harness
8
8
  # Manages provider switching and fallback logic
9
9
  class ProviderManager
10
+ include Aidp::MessageDisplay
11
+
10
12
  def initialize(configuration, prompt: TTY::Prompt.new)
11
13
  @configuration = configuration
12
14
  @prompt = prompt
@@ -31,6 +33,9 @@ module Aidp
31
33
  @model_fallback_chains = {}
32
34
  @model_switching_enabled = true
33
35
  @model_weights = {}
36
+ @unavailable_cache = {}
37
+ @binary_check_cache = {}
38
+ @binary_check_ttl = 300 # seconds
34
39
  initialize_fallback_chains
35
40
  initialize_provider_health
36
41
  initialize_model_configs
@@ -562,9 +567,39 @@ module Aidp
562
567
 
563
568
  # Check if provider is available (not rate limited, healthy, circuit breaker closed)
564
569
  def is_provider_available?(provider_name)
565
- !is_rate_limited?(provider_name) &&
566
- is_provider_healthy?(provider_name) &&
567
- !is_provider_circuit_breaker_open?(provider_name)
570
+ cli_ok, _reason = provider_cli_available?(provider_name)
571
+ return false unless cli_ok
572
+ return false if is_rate_limited?(provider_name)
573
+ return false unless is_provider_healthy?(provider_name)
574
+ return false if is_provider_circuit_breaker_open?(provider_name)
575
+ true
576
+ end
577
+
578
+ # Mark provider unhealthy (auth or generic) and optionally open circuit breaker
579
+ def mark_provider_unhealthy(provider_name, reason: "manual", open_circuit: true)
580
+ return unless @provider_health[provider_name]
581
+ health = @provider_health[provider_name]
582
+ health[:status] = (reason == "auth") ? "unhealthy_auth" : "unhealthy"
583
+ health[:last_updated] = Time.now
584
+ health[:unhealthy_reason] = reason
585
+ if open_circuit
586
+ health[:circuit_breaker_open] = true
587
+ health[:circuit_breaker_opened_at] = Time.now
588
+ log_circuit_breaker_event(provider_name, "opened")
589
+ end
590
+ end
591
+
592
+ def mark_provider_auth_failure(provider_name)
593
+ mark_provider_unhealthy(provider_name, reason: "auth", open_circuit: true)
594
+ end
595
+
596
+ # Mark provider unhealthy specifically due to failure exhaustion (non-auth)
597
+ def mark_provider_failure_exhausted(provider_name)
598
+ return unless @provider_health[provider_name]
599
+ health = @provider_health[provider_name]
600
+ # Don't override more critical states (auth or circuit already open)
601
+ return if health[:unhealthy_reason] == "auth"
602
+ mark_provider_unhealthy(provider_name, reason: "fail_exhausted", open_circuit: true)
568
603
  end
569
604
 
570
605
  # Check if model is rate limited
@@ -950,6 +985,198 @@ module Aidp
950
985
  @provider_metrics.dup
951
986
  end
952
987
 
988
+ # Determine whether a provider CLI/binary appears installed
989
+ def provider_installed?(provider_name)
990
+ return @unavailable_cache[provider_name] unless @unavailable_cache[provider_name].nil?
991
+ installed = true
992
+ begin
993
+ case provider_name
994
+ when "anthropic", "claude"
995
+ # Prefer direct binary probe instead of Anthropic.available? (which uses which internally)
996
+ path = begin
997
+ Aidp::Util.which("claude")
998
+ rescue
999
+ nil
1000
+ end
1001
+ installed = !path.nil?
1002
+ when "cursor"
1003
+ require_relative "../providers/cursor"
1004
+ installed = Aidp::Providers::Cursor.available?
1005
+ end
1006
+ rescue LoadError
1007
+ installed = false
1008
+ end
1009
+ @unavailable_cache[provider_name] = installed
1010
+ end
1011
+
1012
+ # Attempt to run a provider's CLI with --version (or no-op) to verify executable health
1013
+ def provider_cli_available?(provider_name)
1014
+ normalized = normalize_provider_name(provider_name)
1015
+
1016
+ # Handle test environment overrides
1017
+ if defined?(RSpec) || ENV["RSPEC_RUNNING"]
1018
+ # Force claude to be missing for testing
1019
+ if ENV["AIDP_FORCE_CLAUDE_MISSING"] == "1" && normalized == "claude"
1020
+ return [false, "binary_missing"]
1021
+ end
1022
+ # Force claude to be available for testing
1023
+ if ENV["AIDP_FORCE_CLAUDE_AVAILABLE"] == "1" && normalized == "claude"
1024
+ return [true, "available"]
1025
+ end
1026
+ end
1027
+
1028
+ cache_key = "#{provider_name}:#{normalized}"
1029
+ cached = @binary_check_cache[cache_key]
1030
+ if cached && (Time.now - cached[:checked_at] < @binary_check_ttl)
1031
+ return [cached[:ok], cached[:reason]]
1032
+ end
1033
+ # Map normalized provider -> binary
1034
+ binary = case normalized
1035
+ when "claude" then "claude"
1036
+ when "cursor" then "cursor"
1037
+ when "gemini" then "gemini"
1038
+ when "macos" then nil # passthrough; no direct binary expected
1039
+ end
1040
+ unless binary
1041
+ @binary_check_cache[cache_key] = {ok: true, reason: nil, checked_at: Time.now}
1042
+ return [true, nil]
1043
+ end
1044
+ path = begin
1045
+ Aidp::Util.which(binary)
1046
+ rescue
1047
+ nil
1048
+ end
1049
+ unless path
1050
+ @binary_check_cache[cache_key] = {ok: false, reason: "binary_missing", checked_at: Time.now}
1051
+ return [false, "binary_missing"]
1052
+ end
1053
+ # Light command execution to ensure it responds quickly
1054
+ ok = true
1055
+ reason = nil
1056
+ begin
1057
+ # Use IO.popen to avoid shell injection and impose a short timeout
1058
+ r, w = IO.pipe
1059
+ pid = Process.spawn(binary, "--version", out: w, err: w)
1060
+ w.close
1061
+ deadline = Time.now + 3
1062
+ status = nil
1063
+ while Time.now < deadline
1064
+ pid_done, status = Process.waitpid2(pid, Process::WNOHANG)
1065
+ break if pid_done
1066
+ sleep 0.05
1067
+ end
1068
+ unless status
1069
+ # Timeout -> kill
1070
+ begin
1071
+ Process.kill("TERM", pid)
1072
+ rescue
1073
+ nil
1074
+ end
1075
+ sleep 0.1
1076
+ begin
1077
+ Process.kill("KILL", pid)
1078
+ rescue
1079
+ nil
1080
+ end
1081
+ ok = false
1082
+ reason = "binary_timeout"
1083
+ end
1084
+ output = r.read.to_s
1085
+ r.close
1086
+ if ok && output.strip.empty?
1087
+ # Some CLIs require just calling without args; treat empty as still OK
1088
+ ok = true
1089
+ end
1090
+ rescue => e
1091
+ ok = false
1092
+ reason = e.class.name.downcase.include?("enoent") ? "binary_missing" : "binary_error"
1093
+ end
1094
+ @binary_check_cache[cache_key] = {ok: ok, reason: reason, checked_at: Time.now}
1095
+ [ok, reason]
1096
+ end
1097
+
1098
+ # Summarize health and metrics for dashboard/CLI display
1099
+ def health_dashboard
1100
+ now = Time.now
1101
+ statuses = get_provider_health_status
1102
+ metrics = all_metrics
1103
+ configured = @configuration.configured_providers
1104
+ # Ensure fresh binary probe results in test mode so stubs of Aidp::Util.which take effect
1105
+ if defined?(RSpec) || ENV["RSPEC_RUNNING"]
1106
+ @binary_check_cache.clear
1107
+ end
1108
+ rows_by_normalized = {}
1109
+ configured.each do |prov|
1110
+ # Temporarily hide macos provider until it's user-configurable
1111
+ next if prov == "macos"
1112
+ normalized = normalize_provider_name(prov)
1113
+ cli_ok_prefetch, cli_reason_prefetch = provider_cli_available?(prov)
1114
+ h = statuses[prov] || {}
1115
+ m = metrics[prov] || {}
1116
+ rl = @rate_limit_info[prov]
1117
+ reset_in = (rl && rl[:reset_time]) ? [(rl[:reset_time] - now).to_i, 0].max : nil
1118
+ cb_remaining = if h[:circuit_breaker_open] && h[:circuit_breaker_opened_at]
1119
+ elapsed = now - h[:circuit_breaker_opened_at]
1120
+ rem = @circuit_breaker_timeout - elapsed
1121
+ rem.positive? ? rem.to_i : 0
1122
+ end
1123
+ row = {
1124
+ provider: normalized,
1125
+ installed: provider_installed?(prov),
1126
+ status: h[:status] || (provider_installed?(prov) ? "unknown" : "uninstalled"),
1127
+ unhealthy_reason: h[:unhealthy_reason],
1128
+ available: false, # will set true below only if all checks pass
1129
+ circuit_breaker: h[:circuit_breaker_open] ? "open" : "closed",
1130
+ circuit_breaker_remaining: cb_remaining,
1131
+ rate_limited: !!rl,
1132
+ rate_limit_reset_in: reset_in,
1133
+ total_requests: m[:total_requests] || 0,
1134
+ failed_requests: m[:failed_requests] || 0,
1135
+ success_requests: m[:successful_requests] || 0,
1136
+ total_tokens: m[:total_tokens] || 0,
1137
+ last_used: m[:last_used]
1138
+ }
1139
+ # Incorporate CLI check outcome into reason/availability if failing
1140
+ unless cli_ok_prefetch
1141
+ row[:available] = false
1142
+ row[:unhealthy_reason] ||= cli_reason_prefetch
1143
+ row[:status] = "unhealthy" if row[:status] == "healthy" || row[:status] == "healthy_auth"
1144
+ end
1145
+ if cli_ok_prefetch && is_provider_available?(prov)
1146
+ row[:available] = true
1147
+ end
1148
+ if (existing = rows_by_normalized[normalized])
1149
+ # Merge metrics: sum counts/tokens, keep most severe status, earliest unhealthy reason if any
1150
+ existing[:total_requests] += row[:total_requests]
1151
+ existing[:failed_requests] += row[:failed_requests]
1152
+ existing[:success_requests] += row[:success_requests]
1153
+ existing[:total_tokens] += row[:total_tokens]
1154
+ # If either unavailable then mark unavailable
1155
+ existing[:available] &&= row[:available]
1156
+ # Prefer an unhealthy or circuit breaker status over healthy
1157
+ existing[:status] = merge_status_priority(existing[:status], row[:status])
1158
+ existing[:unhealthy_reason] ||= row[:unhealthy_reason]
1159
+ # Circuit breaker open takes precedence
1160
+ if row[:circuit_breaker] == "open"
1161
+ existing[:circuit_breaker] = "open"
1162
+ existing[:circuit_breaker_remaining] = [existing[:circuit_breaker_remaining].to_i, row[:circuit_breaker_remaining].to_i].max
1163
+ end
1164
+ # Rate limited if any underlying
1165
+ if row[:rate_limited]
1166
+ existing[:rate_limited] = true
1167
+ existing[:rate_limit_reset_in] = [existing[:rate_limit_reset_in].to_i, row[:rate_limit_reset_in].to_i].max
1168
+ end
1169
+ # Keep most recent last_used
1170
+ if row[:last_used] && (!existing[:last_used] || row[:last_used] > existing[:last_used])
1171
+ existing[:last_used] = row[:last_used]
1172
+ end
1173
+ else
1174
+ rows_by_normalized[normalized] = row
1175
+ end
1176
+ end
1177
+ rows_by_normalized.values
1178
+ end
1179
+
953
1180
  # Get provider history
954
1181
  def provider_history
955
1182
  @provider_history.dup
@@ -1006,7 +1233,9 @@ module Aidp
1006
1233
  circuit_breaker_open: health[:circuit_breaker_open],
1007
1234
  last_updated: health[:last_updated],
1008
1235
  last_used: health[:last_used],
1009
- last_rate_limited: health[:last_rate_limited]
1236
+ last_rate_limited: health[:last_rate_limited],
1237
+ circuit_breaker_opened_at: health[:circuit_breaker_opened_at],
1238
+ unhealthy_reason: health[:unhealthy_reason]
1010
1239
  }
1011
1240
  end
1012
1241
  end
@@ -1203,17 +1432,23 @@ module Aidp
1203
1432
 
1204
1433
  private
1205
1434
 
1206
- def display_message(message, type: :info)
1207
- color = case type
1208
- when :error then :red
1209
- when :success then :green
1210
- when :warning then :yellow
1211
- when :info then :blue
1212
- when :highlight then :cyan
1213
- when :muted then :bright_black
1214
- else :white
1215
- end
1216
- @prompt.say(message, color: color)
1435
+ # Normalize provider naming for display (hide legacy 'anthropic')
1436
+ def normalize_provider_name(name)
1437
+ return "claude" if name == "anthropic"
1438
+ name
1439
+ end
1440
+
1441
+ # Status priority for merging duplicate normalized providers
1442
+ def merge_status_priority(a, b)
1443
+ order = {
1444
+ "circuit_breaker_open" => 5,
1445
+ "unhealthy_auth" => 4,
1446
+ "unhealthy" => 3,
1447
+ "unknown" => 2,
1448
+ "healthy" => 1,
1449
+ nil => 0
1450
+ }
1451
+ ((order[a] || 0) >= (order[b] || 0)) ? a : b
1217
1452
  end
1218
1453
 
1219
1454
  public
@@ -15,6 +15,8 @@ module Aidp
15
15
  module Harness
16
16
  # Main harness runner that orchestrates the execution loop
17
17
  class Runner
18
+ include Aidp::MessageDisplay
19
+
18
20
  # Harness execution states
19
21
  STATES = {
20
22
  idle: "idle",
@@ -38,7 +40,7 @@ module Aidp
38
40
  @start_time = nil
39
41
  @current_step = nil
40
42
  @current_provider = nil
41
- @user_input = options[:user_input] || {} # Include user input from workflow selection
43
+ @user_input = options[:user_input] || {} # Include user input from workflow selection
42
44
  @execution_log = []
43
45
  @prompt = options[:prompt] || TTY::Prompt.new
44
46
 
@@ -412,19 +414,6 @@ module Aidp
412
414
  end
413
415
 
414
416
  private
415
-
416
- def display_message(message, type: :info)
417
- color = case type
418
- when :error then :red
419
- when :success then :green
420
- when :warning then :yellow
421
- when :info then :blue
422
- when :highlight then :cyan
423
- when :muted then :bright_black
424
- else :white
425
- end
426
- @prompt.say(message, color: color)
427
- end
428
417
  end
429
418
  end
430
419
  end
@@ -7,6 +7,8 @@ module Aidp
7
7
  # Simple, focused user interface for collecting feedback
8
8
  # Replaces the bloated UserInterface with minimal, clean code
9
9
  class SimpleUserInterface
10
+ include Aidp::MessageDisplay
11
+
10
12
  def initialize(prompt: TTY::Prompt.new)
11
13
  @prompt = prompt
12
14
  end
@@ -26,21 +28,6 @@ module Aidp
26
28
 
27
29
  private
28
30
 
29
- # Helper method for consistent message display using TTY::Prompt
30
- def display_message(message, type: :info)
31
- color = case type
32
- when :error then :red
33
- when :success then :green
34
- when :warning then :yellow
35
- when :info then :blue
36
- when :highlight then :cyan
37
- when :muted then :bright_black
38
- else :white
39
- end
40
-
41
- @prompt.say(message, color: color)
42
- end
43
-
44
31
  def show_context(context)
45
32
  display_message("\n🤖 Agent needs feedback", type: :info)
46
33
  display_message("Context: #{context[:description]}", type: :info) if context[:description]