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,13 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "tty-prompt"
3
4
  require_relative "provider_factory"
4
5
 
5
6
  module Aidp
6
7
  module Harness
7
8
  # Manages provider switching and fallback logic
8
9
  class ProviderManager
9
- def initialize(configuration)
10
+ include Aidp::MessageDisplay
11
+
12
+ def initialize(configuration, prompt: TTY::Prompt.new)
10
13
  @configuration = configuration
14
+ @prompt = prompt
11
15
  @current_provider = nil
12
16
  @current_model = nil
13
17
  @provider_history = []
@@ -29,6 +33,9 @@ module Aidp
29
33
  @model_fallback_chains = {}
30
34
  @model_switching_enabled = true
31
35
  @model_weights = {}
36
+ @unavailable_cache = {}
37
+ @binary_check_cache = {}
38
+ @binary_check_ttl = 300 # seconds
32
39
  initialize_fallback_chains
33
40
  initialize_provider_health
34
41
  initialize_model_configs
@@ -560,9 +567,39 @@ module Aidp
560
567
 
561
568
  # Check if provider is available (not rate limited, healthy, circuit breaker closed)
562
569
  def is_provider_available?(provider_name)
563
- !is_rate_limited?(provider_name) &&
564
- is_provider_healthy?(provider_name) &&
565
- !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)
566
603
  end
567
604
 
568
605
  # Check if model is rate limited
@@ -948,6 +985,198 @@ module Aidp
948
985
  @provider_metrics.dup
949
986
  end
950
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
+
951
1180
  # Get provider history
952
1181
  def provider_history
953
1182
  @provider_history.dup
@@ -1004,7 +1233,9 @@ module Aidp
1004
1233
  circuit_breaker_open: health[:circuit_breaker_open],
1005
1234
  last_updated: health[:last_updated],
1006
1235
  last_used: health[:last_used],
1007
- 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]
1008
1239
  }
1009
1240
  end
1010
1241
  end
@@ -1199,20 +1430,43 @@ module Aidp
1199
1430
  [delay, 60].min # Cap at 60 seconds
1200
1431
  end
1201
1432
 
1433
+ private
1434
+
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
1452
+ end
1453
+
1454
+ public
1455
+
1202
1456
  # Log provider switch
1203
1457
  def log_provider_switch(from_provider, to_provider, reason, context)
1204
- puts "🔄 Provider switch: #{from_provider} → #{to_provider} (#{reason})"
1458
+ display_message("🔄 Provider switch: #{from_provider} → #{to_provider} (#{reason})", type: :info)
1205
1459
  if context.any?
1206
- puts " Context: #{context.inspect}"
1460
+ display_message(" Context: #{context.inspect}", type: :muted)
1207
1461
  end
1208
1462
  end
1209
1463
 
1210
1464
  # Log no providers available
1211
1465
  def log_no_providers_available(reason, context)
1212
- puts "❌ No providers available for switching (#{reason})"
1213
- puts " All providers are rate limited, unhealthy, or circuit breaker open"
1466
+ display_message("❌ No providers available for switching (#{reason})", type: :error)
1467
+ display_message(" All providers are rate limited, unhealthy, or circuit breaker open", type: :warning)
1214
1468
  if context.any?
1215
- puts " Context: #{context.inspect}"
1469
+ display_message(" Context: #{context.inspect}", type: :muted)
1216
1470
  end
1217
1471
  end
1218
1472
 
@@ -1220,26 +1474,26 @@ module Aidp
1220
1474
  def log_circuit_breaker_event(provider_name, event)
1221
1475
  case event
1222
1476
  when "opened"
1223
- puts "🔴 Circuit breaker opened for provider: #{provider_name}"
1477
+ display_message("🔴 Circuit breaker opened for provider: #{provider_name}", type: :error)
1224
1478
  when "reset"
1225
- puts "🟢 Circuit breaker reset for provider: #{provider_name}"
1479
+ display_message("🟢 Circuit breaker reset for provider: #{provider_name}", type: :success)
1226
1480
  end
1227
1481
  end
1228
1482
 
1229
1483
  # Log model switch
1230
1484
  def log_model_switch(from_model, to_model, reason, context)
1231
- puts "🔄 Model switch: #{from_model} → #{to_model} (#{reason})"
1485
+ display_message("🔄 Model switch: #{from_model} → #{to_model} (#{reason})", type: :info)
1232
1486
  if context.any?
1233
- puts " Context: #{context.inspect}"
1487
+ display_message(" Context: #{context.inspect}", type: :muted)
1234
1488
  end
1235
1489
  end
1236
1490
 
1237
1491
  # Log no models available
1238
1492
  def log_no_models_available(provider_name, reason, context)
1239
- puts "❌ No models available for provider #{provider_name} (#{reason})"
1240
- puts " All models are rate limited, unhealthy, or circuit breaker open"
1493
+ display_message("❌ No models available for provider #{provider_name} (#{reason})", type: :error)
1494
+ display_message(" All models are rate limited, unhealthy, or circuit breaker open", type: :warning)
1241
1495
  if context.any?
1242
- puts " Context: #{context.inspect}"
1496
+ display_message(" Context: #{context.inspect}", type: :muted)
1243
1497
  end
1244
1498
  end
1245
1499
 
@@ -1247,9 +1501,9 @@ module Aidp
1247
1501
  def log_model_circuit_breaker_event(provider_name, model_name, event)
1248
1502
  case event
1249
1503
  when "opened"
1250
- puts "🔴 Circuit breaker opened for model: #{provider_name}:#{model_name}"
1504
+ display_message("🔴 Circuit breaker opened for model: #{provider_name}:#{model_name}", type: :error)
1251
1505
  when "reset"
1252
- puts "🟢 Circuit breaker reset for model: #{provider_name}:#{model_name}"
1506
+ display_message("🟢 Circuit breaker reset for model: #{provider_name}:#{model_name}", type: :success)
1253
1507
  end
1254
1508
  end
1255
1509
 
@@ -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",
@@ -27,6 +29,9 @@ module Aidp
27
29
  error: "error"
28
30
  }.freeze
29
31
 
32
+ # Public accessors for testing and integration
33
+ attr_reader :current_provider, :current_step, :user_input, :execution_log, :provider_manager
34
+
30
35
  def initialize(project_dir, mode = :analyze, options = {})
31
36
  @project_dir = project_dir
32
37
  @mode = mode.to_sym
@@ -35,8 +40,9 @@ module Aidp
35
40
  @start_time = nil
36
41
  @current_step = nil
37
42
  @current_provider = nil
38
- @user_input = options[:user_input] || {} # Include user input from workflow selection
43
+ @user_input = options[:user_input] || {} # Include user input from workflow selection
39
44
  @execution_log = []
45
+ @prompt = options[:prompt] || TTY::Prompt.new
40
46
 
41
47
  # Store workflow configuration
42
48
  @selected_steps = options[:selected_steps]
@@ -46,7 +52,7 @@ module Aidp
46
52
  @configuration = Configuration.new(project_dir)
47
53
  @state_manager = StateManager.new(project_dir, @mode)
48
54
  @condition_detector = ConditionDetector.new
49
- @provider_manager = ProviderManager.new(@configuration)
55
+ @provider_manager = ProviderManager.new(@configuration, prompt: @prompt)
50
56
  @user_interface = SimpleUserInterface.new
51
57
  @error_handler = ErrorHandler.new(@provider_manager, @configuration)
52
58
  @status_display = StatusDisplay.new
@@ -96,8 +102,8 @@ module Aidp
96
102
  log_execution("Harness completed successfully - all criteria met", completion_status)
97
103
  else
98
104
  log_execution("Steps completed but completion criteria not met", completion_status)
99
- puts "\n⚠️ All steps completed but some completion criteria not met:"
100
- puts completion_status[:summary]
105
+ display_message("\n⚠️ All steps completed but some completion criteria not met:", type: :warning)
106
+ display_message(completion_status[:summary], type: :info)
101
107
 
102
108
  # Ask user if they want to continue anyway
103
109
  if @user_interface.get_confirmation("Continue anyway? This may indicate issues that should be addressed.", default: false)
@@ -181,9 +187,9 @@ module Aidp
181
187
  def get_mode_runner
182
188
  case @mode
183
189
  when :analyze
184
- Aidp::Analyze::Runner.new(@project_dir, self)
190
+ Aidp::Analyze::Runner.new(@project_dir, self, prompt: TTY::Prompt.new)
185
191
  when :execute
186
- Aidp::Execute::Runner.new(@project_dir, self)
192
+ Aidp::Execute::Runner.new(@project_dir, self, prompt: TTY::Prompt.new)
187
193
  else
188
194
  raise ArgumentError, "Unsupported mode: #{@mode}"
189
195
  end
@@ -406,6 +412,8 @@ module Aidp
406
412
  "Harness finished in state: #{@state}"
407
413
  end
408
414
  end
415
+
416
+ private
409
417
  end
410
418
  end
411
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
@@ -27,9 +29,9 @@ module Aidp
27
29
  private
28
30
 
29
31
  def show_context(context)
30
- puts "\n🤖 Agent needs feedback"
31
- puts "Context: #{context[:description]}" if context[:description]
32
- puts ""
32
+ display_message("\n🤖 Agent needs feedback", type: :info)
33
+ display_message("Context: #{context[:description]}", type: :info) if context[:description]
34
+ display_message("", type: :info)
33
35
  end
34
36
 
35
37
  def ask_question(question_data)
@@ -39,7 +41,7 @@ module Aidp
39
41
  required = question_data[:required] != false
40
42
  options = question_data[:options]
41
43
 
42
- puts "\n#{question}"
44
+ display_message("\n#{question}", type: :info)
43
45
 
44
46
  case type
45
47
  when "text"