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.
- checksums.yaml +4 -4
- data/README.md +194 -25
- data/lib/aidp/analyze/kb_inspector.rb +2 -15
- data/lib/aidp/analyze/progress.rb +2 -1
- data/lib/aidp/analyze/ruby_maat_integration.rb +2 -15
- data/lib/aidp/analyze/runner.rb +64 -20
- data/lib/aidp/analyze/steps.rb +10 -8
- data/lib/aidp/analyze/tree_sitter_grammar_loader.rb +2 -13
- data/lib/aidp/analyze/tree_sitter_scan.rb +2 -13
- data/lib/aidp/cli/checkpoint_command.rb +98 -0
- data/lib/aidp/cli/first_run_wizard.rb +65 -94
- data/lib/aidp/cli/jobs_command.rb +249 -34
- data/lib/aidp/cli.rb +312 -38
- data/lib/aidp/config.rb +5 -8
- data/lib/aidp/debug_logger.rb +4 -4
- data/lib/aidp/debug_mixin.rb +11 -4
- data/lib/aidp/execute/checkpoint.rb +282 -0
- data/lib/aidp/execute/checkpoint_display.rb +221 -0
- data/lib/aidp/execute/progress.rb +2 -1
- data/lib/aidp/execute/prompt_manager.rb +62 -0
- data/lib/aidp/execute/runner.rb +53 -24
- data/lib/aidp/execute/steps.rb +36 -27
- data/lib/aidp/execute/work_loop_runner.rb +308 -0
- data/lib/aidp/execute/workflow_selector.rb +26 -17
- data/lib/aidp/harness/condition_detector.rb +4 -4
- data/lib/aidp/harness/config_schema.rb +40 -0
- data/lib/aidp/harness/config_validator.rb +3 -6
- data/lib/aidp/harness/configuration.rb +35 -1
- data/lib/aidp/harness/enhanced_runner.rb +22 -1
- data/lib/aidp/harness/error_handler.rb +103 -28
- data/lib/aidp/harness/provider_factory.rb +4 -1
- data/lib/aidp/harness/provider_manager.rb +250 -15
- data/lib/aidp/harness/runner.rb +3 -14
- data/lib/aidp/harness/simple_user_interface.rb +2 -15
- data/lib/aidp/harness/status_display.rb +12 -17
- data/lib/aidp/harness/test_runner.rb +83 -0
- data/lib/aidp/harness/ui/enhanced_tui.rb +2 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +22 -4
- data/lib/aidp/harness/ui/error_handler.rb +4 -0
- data/lib/aidp/harness/ui/frame_manager.rb +10 -8
- data/lib/aidp/harness/ui/job_monitor.rb +2 -0
- data/lib/aidp/harness/ui/navigation/main_menu.rb +4 -2
- data/lib/aidp/harness/ui/navigation/menu_item.rb +1 -0
- data/lib/aidp/harness/ui/navigation/menu_state.rb +1 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +2 -0
- data/lib/aidp/harness/ui/progress_display.rb +8 -12
- data/lib/aidp/harness/ui/question_collector.rb +2 -0
- data/lib/aidp/harness/ui/spinner_group.rb +2 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +1 -1
- data/lib/aidp/harness/ui/status_manager.rb +4 -2
- data/lib/aidp/harness/ui/status_widget.rb +3 -1
- data/lib/aidp/harness/ui/workflow_controller.rb +2 -0
- data/lib/aidp/harness/user_interface.rb +12 -17
- data/lib/aidp/jobs/background_runner.rb +278 -0
- data/lib/aidp/message_display.rb +48 -0
- data/lib/aidp/provider_manager.rb +3 -1
- data/lib/aidp/providers/anthropic.rb +100 -17
- data/lib/aidp/providers/base.rb +42 -11
- data/lib/aidp/providers/codex.rb +248 -0
- data/lib/aidp/providers/cursor.rb +35 -42
- data/lib/aidp/providers/gemini.rb +25 -15
- data/lib/aidp/providers/github_copilot.rb +41 -42
- data/lib/aidp/providers/opencode.rb +34 -41
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/definitions.rb +357 -0
- data/lib/aidp/workflows/selector.rb +171 -0
- data/lib/aidp.rb +12 -0
- data/templates/planning/generate_llm_style_guide.md +119 -0
- metadata +38 -26
- /data/templates/{ANALYZE/02_ARCHITECTURE_ANALYSIS.md → analysis/analyze_architecture.md} +0 -0
- /data/templates/{ANALYZE/05_DOCUMENTATION_ANALYSIS.md → analysis/analyze_documentation.md} +0 -0
- /data/templates/{ANALYZE/04_FUNCTIONALITY_ANALYSIS.md → analysis/analyze_functionality.md} +0 -0
- /data/templates/{ANALYZE/01_REPOSITORY_ANALYSIS.md → analysis/analyze_repository.md} +0 -0
- /data/templates/{ANALYZE/06_STATIC_ANALYSIS.md → analysis/analyze_static_code.md} +0 -0
- /data/templates/{ANALYZE/03_TEST_ANALYSIS.md → analysis/analyze_tests.md} +0 -0
- /data/templates/{ANALYZE/07_REFACTORING_RECOMMENDATIONS.md → analysis/recommend_refactoring.md} +0 -0
- /data/templates/{ANALYZE/06a_tree_sitter_scan.md → analysis/scan_with_tree_sitter.md} +0 -0
- /data/templates/{EXECUTE/11_STATIC_ANALYSIS.md → implementation/configure_static_analysis.md} +0 -0
- /data/templates/{EXECUTE/14_DOCS_PORTAL.md → implementation/create_documentation_portal.md} +0 -0
- /data/templates/{EXECUTE/10_IMPLEMENTATION_AGENT.md → implementation/implement_features.md} +0 -0
- /data/templates/{EXECUTE/13_DELIVERY_ROLLOUT.md → implementation/plan_delivery.md} +0 -0
- /data/templates/{EXECUTE/15_POST_RELEASE.md → implementation/review_post_release.md} +0 -0
- /data/templates/{EXECUTE/09_SCAFFOLDING_DEVEX.md → implementation/setup_scaffolding.md} +0 -0
- /data/templates/{EXECUTE/02A_ARCH_GATE_QUESTIONS.md → planning/ask_architecture_questions.md} +0 -0
- /data/templates/{EXECUTE/00_PRD.md → planning/create_prd.md} +0 -0
- /data/templates/{EXECUTE/08_TASKS.md → planning/create_tasks.md} +0 -0
- /data/templates/{EXECUTE/04_DOMAIN_DECOMPOSITION.md → planning/decompose_domain.md} +0 -0
- /data/templates/{EXECUTE/01_NFRS.md → planning/define_nfrs.md} +0 -0
- /data/templates/{EXECUTE/05_CONTRACTS.md → planning/design_apis.md} +0 -0
- /data/templates/{EXECUTE/02_ARCHITECTURE.md → planning/design_architecture.md} +0 -0
- /data/templates/{EXECUTE/06_THREAT_MODEL.md → planning/design_data_model.md} +0 -0
- /data/templates/{EXECUTE/03_ADR_FACTORY.md → planning/generate_adrs.md} +0 -0
- /data/templates/{EXECUTE/12_OBSERVABILITY_SLOS.md → planning/plan_observability.md} +0 -0
- /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?
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
attempt
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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: :
|
584
|
-
reason: "Authentication
|
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
|
-
|
566
|
-
|
567
|
-
|
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
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
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
|
data/lib/aidp/harness/runner.rb
CHANGED
@@ -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] || {}
|
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]
|