aidp 0.10.0 → 0.12.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/mcp_dashboard.rb +205 -0
- data/lib/aidp/cli.rb +517 -43
- 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_info.rb +366 -0
- 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 +44 -5
- 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/guided_agent.rb +400 -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 +41 -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
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module Aidp
|
|
8
|
+
module Harness
|
|
9
|
+
# Stores detailed information about AI providers gathered from their CLI tools
|
|
10
|
+
class ProviderInfo
|
|
11
|
+
attr_reader :provider_name, :info_file_path
|
|
12
|
+
|
|
13
|
+
def initialize(provider_name, root_dir = nil)
|
|
14
|
+
@provider_name = provider_name
|
|
15
|
+
@root_dir = root_dir || Dir.pwd
|
|
16
|
+
@info_file_path = File.join(@root_dir, ".aidp", "providers", "#{provider_name}_info.yml")
|
|
17
|
+
ensure_directory_exists
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Gather information about the provider by introspecting its CLI
|
|
21
|
+
def gather_info
|
|
22
|
+
info = {
|
|
23
|
+
provider: @provider_name,
|
|
24
|
+
last_checked: Time.now.iso8601,
|
|
25
|
+
cli_available: false,
|
|
26
|
+
help_output: nil,
|
|
27
|
+
capabilities: {},
|
|
28
|
+
permission_modes: [],
|
|
29
|
+
mcp_support: false,
|
|
30
|
+
mcp_servers: [],
|
|
31
|
+
auth_method: nil,
|
|
32
|
+
flags: {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Try to get help output from the provider CLI
|
|
36
|
+
help_output = fetch_help_output
|
|
37
|
+
if help_output
|
|
38
|
+
info[:cli_available] = true
|
|
39
|
+
info[:help_output] = help_output
|
|
40
|
+
info.merge!(parse_help_output(help_output))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Try to get MCP server list if supported
|
|
44
|
+
if info[:mcp_support]
|
|
45
|
+
mcp_servers = fetch_mcp_servers
|
|
46
|
+
info[:mcp_servers] = mcp_servers if mcp_servers
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
save_info(info)
|
|
50
|
+
info
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Load stored provider info
|
|
54
|
+
def load_info
|
|
55
|
+
return nil unless File.exist?(@info_file_path)
|
|
56
|
+
|
|
57
|
+
YAML.safe_load_file(@info_file_path, permitted_classes: [Time, Symbol])
|
|
58
|
+
rescue => e
|
|
59
|
+
warn "Failed to load provider info for #{@provider_name}: #{e.message}"
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get provider info, refreshing if needed
|
|
64
|
+
def get_info(force_refresh: false, max_age: 86400)
|
|
65
|
+
existing_info = load_info
|
|
66
|
+
|
|
67
|
+
# Refresh if forced, missing, or stale
|
|
68
|
+
if force_refresh || existing_info.nil? || info_stale?(existing_info, max_age)
|
|
69
|
+
gather_info
|
|
70
|
+
else
|
|
71
|
+
existing_info
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if provider supports MCP servers
|
|
76
|
+
def supports_mcp?
|
|
77
|
+
info = load_info
|
|
78
|
+
return false unless info
|
|
79
|
+
|
|
80
|
+
info[:mcp_support] == true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get permission modes available
|
|
84
|
+
def permission_modes
|
|
85
|
+
info = load_info
|
|
86
|
+
return [] unless info
|
|
87
|
+
|
|
88
|
+
info[:permission_modes] || []
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get authentication method
|
|
92
|
+
def auth_method
|
|
93
|
+
info = load_info
|
|
94
|
+
return nil unless info
|
|
95
|
+
|
|
96
|
+
info[:auth_method]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get available flags/options
|
|
100
|
+
def available_flags
|
|
101
|
+
info = load_info
|
|
102
|
+
return {} unless info
|
|
103
|
+
|
|
104
|
+
info[:flags] || {}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Get configured MCP servers
|
|
108
|
+
def mcp_servers
|
|
109
|
+
info = load_info
|
|
110
|
+
return [] unless info
|
|
111
|
+
|
|
112
|
+
info[:mcp_servers] || []
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Check if provider has MCP servers configured
|
|
116
|
+
def has_mcp_servers?
|
|
117
|
+
mcp_servers.any?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def ensure_directory_exists
|
|
123
|
+
dir = File.dirname(@info_file_path)
|
|
124
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def save_info(info)
|
|
128
|
+
File.write(@info_file_path, YAML.dump(info))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def info_stale?(info, max_age)
|
|
132
|
+
return true unless info[:last_checked]
|
|
133
|
+
|
|
134
|
+
last_checked = Time.parse(info[:last_checked].to_s)
|
|
135
|
+
(Time.now - last_checked) > max_age
|
|
136
|
+
rescue
|
|
137
|
+
true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def fetch_help_output
|
|
141
|
+
execute_provider_command("--help")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def fetch_mcp_servers
|
|
145
|
+
binary = get_binary_name
|
|
146
|
+
return nil unless binary
|
|
147
|
+
|
|
148
|
+
# Try different MCP list commands based on provider
|
|
149
|
+
mcp_output = case @provider_name
|
|
150
|
+
when "claude", "anthropic"
|
|
151
|
+
execute_provider_command("mcp", "list")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
return nil unless mcp_output
|
|
155
|
+
|
|
156
|
+
parse_mcp_servers(mcp_output)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def execute_provider_command(*args)
|
|
160
|
+
binary = get_binary_name
|
|
161
|
+
return nil unless binary
|
|
162
|
+
|
|
163
|
+
# Try to find the binary
|
|
164
|
+
path = begin
|
|
165
|
+
Aidp::Util.which(binary)
|
|
166
|
+
rescue
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
return nil unless path
|
|
170
|
+
|
|
171
|
+
# Execute command with timeout
|
|
172
|
+
begin
|
|
173
|
+
r, w = IO.pipe
|
|
174
|
+
pid = Process.spawn(binary, *args, out: w, err: w)
|
|
175
|
+
w.close
|
|
176
|
+
|
|
177
|
+
# Wait with timeout
|
|
178
|
+
deadline = Time.now + 5
|
|
179
|
+
status = nil
|
|
180
|
+
while Time.now < deadline
|
|
181
|
+
pid_done, status = Process.waitpid2(pid, Process::WNOHANG)
|
|
182
|
+
break if pid_done
|
|
183
|
+
sleep 0.05
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Kill if timed out
|
|
187
|
+
unless status
|
|
188
|
+
begin
|
|
189
|
+
Process.kill("TERM", pid)
|
|
190
|
+
sleep 0.1
|
|
191
|
+
Process.kill("KILL", pid)
|
|
192
|
+
rescue
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
return nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
output = r.read
|
|
199
|
+
r.close
|
|
200
|
+
output
|
|
201
|
+
rescue
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def parse_mcp_servers(output)
|
|
207
|
+
servers = []
|
|
208
|
+
return servers unless output
|
|
209
|
+
|
|
210
|
+
# Parse MCP server list output
|
|
211
|
+
# Claude format (as of 2025):
|
|
212
|
+
# dash-api: uvx --from git+https://... - ✓ Connected
|
|
213
|
+
# or
|
|
214
|
+
# server-name: command - ✗ Error message
|
|
215
|
+
#
|
|
216
|
+
# Legacy format:
|
|
217
|
+
# Name Status Description
|
|
218
|
+
# filesystem enabled File system access
|
|
219
|
+
|
|
220
|
+
lines = output.lines
|
|
221
|
+
|
|
222
|
+
# Skip header lines
|
|
223
|
+
lines.reject! { |line| /checking mcp server health/i.match?(line) }
|
|
224
|
+
|
|
225
|
+
lines.each do |line|
|
|
226
|
+
line = line.strip
|
|
227
|
+
next if line.empty?
|
|
228
|
+
|
|
229
|
+
# Try to parse new Claude format: "name: command - ✓ Connected"
|
|
230
|
+
if line =~ /^([^:]+):\s*(.+?)\s*-\s*(✓|✗)\s*(.+)$/
|
|
231
|
+
name = Regexp.last_match(1).strip
|
|
232
|
+
command = Regexp.last_match(2).strip
|
|
233
|
+
status_symbol = Regexp.last_match(3)
|
|
234
|
+
status_text = Regexp.last_match(4).strip
|
|
235
|
+
|
|
236
|
+
servers << {
|
|
237
|
+
name: name,
|
|
238
|
+
status: (status_symbol == "✓") ? "connected" : "error",
|
|
239
|
+
description: command,
|
|
240
|
+
enabled: status_symbol == "✓",
|
|
241
|
+
error: (status_symbol == "✗") ? status_text : nil
|
|
242
|
+
}
|
|
243
|
+
next
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Try to parse legacy table format
|
|
247
|
+
# Skip header line
|
|
248
|
+
next if /Name.*Status/i.match?(line)
|
|
249
|
+
next if /^[-=]+$/.match?(line) # Skip separator lines
|
|
250
|
+
|
|
251
|
+
# Parse table format: columns separated by multiple spaces
|
|
252
|
+
parts = line.split(/\s{2,}/)
|
|
253
|
+
next if parts.size < 2
|
|
254
|
+
|
|
255
|
+
name = parts[0]&.strip
|
|
256
|
+
status = parts[1]&.strip
|
|
257
|
+
description = parts[2..]&.join(" ")&.strip
|
|
258
|
+
|
|
259
|
+
next unless name && !name.empty?
|
|
260
|
+
|
|
261
|
+
servers << {
|
|
262
|
+
name: name,
|
|
263
|
+
status: status || "unknown",
|
|
264
|
+
description: description,
|
|
265
|
+
enabled: status&.downcase == "enabled" || status&.downcase == "connected"
|
|
266
|
+
}
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
servers
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def get_binary_name
|
|
273
|
+
case @provider_name
|
|
274
|
+
when "claude", "anthropic"
|
|
275
|
+
"claude"
|
|
276
|
+
when "cursor"
|
|
277
|
+
"cursor"
|
|
278
|
+
when "gemini"
|
|
279
|
+
"gemini"
|
|
280
|
+
when "codex"
|
|
281
|
+
"codex"
|
|
282
|
+
when "github_copilot"
|
|
283
|
+
"gh"
|
|
284
|
+
when "opencode"
|
|
285
|
+
"opencode"
|
|
286
|
+
else
|
|
287
|
+
@provider_name
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def parse_help_output(help_text)
|
|
292
|
+
parsed = {
|
|
293
|
+
capabilities: {},
|
|
294
|
+
permission_modes: [],
|
|
295
|
+
mcp_support: false,
|
|
296
|
+
auth_method: nil,
|
|
297
|
+
flags: {}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# Check for MCP support
|
|
301
|
+
parsed[:mcp_support] = !!(help_text =~ /mcp|MCP|Model Context Protocol/i)
|
|
302
|
+
|
|
303
|
+
# Extract permission modes
|
|
304
|
+
if help_text =~ /--permission-mode\s+<mode>\s+.*?\(choices:\s*([^)]+)\)/m
|
|
305
|
+
modes = Regexp.last_match(1).split(",").map(&:strip).map { |m| m.gsub(/["']/, "") }
|
|
306
|
+
parsed[:permission_modes] = modes
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Check for dangerous skip permissions
|
|
310
|
+
parsed[:capabilities][:bypass_permissions] = !!(help_text =~ /--dangerously-skip-permissions/)
|
|
311
|
+
|
|
312
|
+
# Check for API key / subscription patterns
|
|
313
|
+
if /--api-key|API_KEY|setup-token|subscription/i.match?(help_text)
|
|
314
|
+
parsed[:auth_method] = if /setup-token|subscription/i.match?(help_text)
|
|
315
|
+
"subscription"
|
|
316
|
+
else
|
|
317
|
+
"api_key"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Extract model configuration
|
|
322
|
+
parsed[:capabilities][:model_selection] = !!(help_text =~ /--model\s+<model>/)
|
|
323
|
+
|
|
324
|
+
# Extract MCP configuration
|
|
325
|
+
parsed[:capabilities][:mcp_config] = !!(help_text =~ /--mcp-config/)
|
|
326
|
+
|
|
327
|
+
# Extract allowed/disallowed tools
|
|
328
|
+
parsed[:capabilities][:tool_restrictions] = !!(help_text =~ /--allowed-tools|--disallowed-tools/)
|
|
329
|
+
|
|
330
|
+
# Extract session management
|
|
331
|
+
parsed[:capabilities][:session_management] = !!(help_text =~ /--continue|--resume|--fork-session/)
|
|
332
|
+
|
|
333
|
+
# Extract output formats
|
|
334
|
+
if help_text =~ /--output-format\s+.*?\(choices:\s*([^)]+)\)/m
|
|
335
|
+
formats = Regexp.last_match(1).split(",").map(&:strip).map { |f| f.gsub(/["']/, "") }
|
|
336
|
+
parsed[:capabilities][:output_formats] = formats
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Extract notable flags
|
|
340
|
+
extract_flags(help_text, parsed[:flags])
|
|
341
|
+
|
|
342
|
+
parsed
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def extract_flags(help_text, flags_hash)
|
|
346
|
+
# Extract all flags with their descriptions
|
|
347
|
+
help_text.scan(/^\s+(--[\w-]+(?:\s+<\w+>)?)\s+(.+?)(?=^\s+(?:--|\w|$))/m).each do |flag, desc|
|
|
348
|
+
flag_name = flag.split.first.gsub(/^--/, "")
|
|
349
|
+
flags_hash[flag_name] = {
|
|
350
|
+
flag: flag.strip,
|
|
351
|
+
description: desc.strip.gsub(/\s+/, " ")
|
|
352
|
+
}
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Also capture short flags
|
|
356
|
+
help_text.scan(/^\s+(-\w),\s+(--[\w-]+(?:\s+<\w+>)?)\s+(.+?)(?=^\s+(?:--|-\w|$))/m).each do |short, long, desc|
|
|
357
|
+
flag_name = long.split.first.gsub(/^--/, "")
|
|
358
|
+
flags_hash[flag_name] ||= {}
|
|
359
|
+
flags_hash[flag_name][:short] = short
|
|
360
|
+
flags_hash[flag_name][:flag] = long.strip
|
|
361
|
+
flags_hash[flag_name][:description] = desc.strip.gsub(/\s+/, " ")
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
@@ -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
|