aidp 0.25.0 ā 0.27.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 +45 -6
- data/lib/aidp/analyze/error_handler.rb +11 -0
- data/lib/aidp/cli/checkpoint_command.rb +198 -0
- data/lib/aidp/cli/config_command.rb +71 -0
- data/lib/aidp/cli/enhanced_input.rb +2 -0
- data/lib/aidp/cli/first_run_wizard.rb +8 -7
- data/lib/aidp/cli/harness_command.rb +102 -0
- data/lib/aidp/cli/jobs_command.rb +3 -3
- data/lib/aidp/cli/mcp_dashboard.rb +4 -3
- data/lib/aidp/cli/models_command.rb +662 -0
- data/lib/aidp/cli/providers_command.rb +223 -0
- data/lib/aidp/cli.rb +35 -456
- data/lib/aidp/daemon/runner.rb +2 -2
- data/lib/aidp/debug_mixin.rb +2 -9
- data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
- data/lib/aidp/execute/checkpoint_display.rb +38 -37
- data/lib/aidp/execute/interactive_repl.rb +2 -1
- data/lib/aidp/execute/prompt_manager.rb +4 -4
- data/lib/aidp/execute/work_loop_runner.rb +253 -56
- data/lib/aidp/execute/workflow_selector.rb +2 -2
- data/lib/aidp/harness/config_loader.rb +20 -11
- data/lib/aidp/harness/config_manager.rb +5 -5
- data/lib/aidp/harness/config_schema.rb +30 -8
- data/lib/aidp/harness/configuration.rb +105 -4
- data/lib/aidp/harness/enhanced_runner.rb +24 -15
- data/lib/aidp/harness/error_handler.rb +26 -5
- data/lib/aidp/harness/filter_strategy.rb +45 -0
- data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
- data/lib/aidp/harness/model_cache.rb +269 -0
- data/lib/aidp/harness/model_discovery_service.rb +259 -0
- data/lib/aidp/harness/model_registry.rb +201 -0
- data/lib/aidp/harness/output_filter.rb +136 -0
- data/lib/aidp/harness/provider_manager.rb +18 -3
- data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
- data/lib/aidp/harness/runner.rb +5 -0
- data/lib/aidp/harness/test_runner.rb +165 -27
- data/lib/aidp/harness/thinking_depth_manager.rb +223 -7
- data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
- data/lib/aidp/logger.rb +35 -5
- data/lib/aidp/providers/adapter.rb +2 -4
- data/lib/aidp/providers/anthropic.rb +141 -128
- data/lib/aidp/providers/base.rb +98 -2
- data/lib/aidp/providers/capability_registry.rb +0 -1
- data/lib/aidp/providers/codex.rb +49 -67
- data/lib/aidp/providers/cursor.rb +71 -59
- data/lib/aidp/providers/gemini.rb +44 -60
- data/lib/aidp/providers/github_copilot.rb +2 -66
- data/lib/aidp/providers/kilocode.rb +24 -80
- data/lib/aidp/providers/opencode.rb +24 -80
- data/lib/aidp/safe_directory.rb +10 -3
- data/lib/aidp/setup/wizard.rb +345 -8
- data/lib/aidp/storage/csv_storage.rb +9 -3
- data/lib/aidp/storage/file_manager.rb +8 -2
- data/lib/aidp/storage/json_storage.rb +9 -3
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +40 -1
- data/lib/aidp/watch/change_request_processor.rb +659 -0
- data/lib/aidp/watch/plan_generator.rb +93 -14
- data/lib/aidp/watch/plan_processor.rb +71 -8
- data/lib/aidp/watch/repository_client.rb +85 -20
- data/lib/aidp/watch/review_processor.rb +3 -3
- data/lib/aidp/watch/runner.rb +37 -0
- data/lib/aidp/watch/state_store.rb +46 -1
- data/lib/aidp/workflows/guided_agent.rb +3 -3
- data/lib/aidp/workstream_executor.rb +5 -2
- data/lib/aidp.rb +4 -0
- data/templates/aidp-development.yml.example +2 -2
- data/templates/aidp-production.yml.example +3 -3
- data/templates/aidp.yml.example +53 -0
- metadata +14 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "filter_strategy"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Harness
|
|
7
|
+
# RSpec-specific output filtering
|
|
8
|
+
class RSpecFilterStrategy < FilterStrategy
|
|
9
|
+
def filter(output, filter_instance)
|
|
10
|
+
case filter_instance.mode
|
|
11
|
+
when :failures_only
|
|
12
|
+
extract_failures_only(output, filter_instance)
|
|
13
|
+
when :minimal
|
|
14
|
+
extract_minimal(output, filter_instance)
|
|
15
|
+
else
|
|
16
|
+
output
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def extract_failures_only(output, filter_instance)
|
|
23
|
+
lines = output.lines
|
|
24
|
+
parts = []
|
|
25
|
+
|
|
26
|
+
# Extract summary line
|
|
27
|
+
if (summary = lines.find { |l| l.match?(/^\d+ examples?, \d+ failures?/) })
|
|
28
|
+
parts << "RSpec Summary:"
|
|
29
|
+
parts << summary
|
|
30
|
+
parts << ""
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Extract failed examples
|
|
34
|
+
in_failure = false
|
|
35
|
+
failure_lines = []
|
|
36
|
+
|
|
37
|
+
lines.each_with_index do |line, index|
|
|
38
|
+
# Start of failure section
|
|
39
|
+
if line.match?(/^Failures:/)
|
|
40
|
+
in_failure = true
|
|
41
|
+
failure_lines << line
|
|
42
|
+
next
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# End of failure section (start of pending/seed info)
|
|
46
|
+
if in_failure && (line.match?(/^Finished in/) || line.match?(/^Pending:/))
|
|
47
|
+
in_failure = false
|
|
48
|
+
break
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
failure_lines << line if in_failure
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if failure_lines.any?
|
|
55
|
+
parts << failure_lines.join
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
parts.join("\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def extract_minimal(output, filter_instance)
|
|
62
|
+
lines = output.lines
|
|
63
|
+
parts = []
|
|
64
|
+
|
|
65
|
+
# Extract only summary and failure locations
|
|
66
|
+
if (summary = lines.find { |l| l.match?(/^\d+ examples?, \d+ failures?/) })
|
|
67
|
+
parts << summary
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Extract failure locations (file:line references)
|
|
71
|
+
failure_locations = lines.select { |l| l.match?(/# \.\/\S+:\d+/) }
|
|
72
|
+
if failure_locations.any?
|
|
73
|
+
parts << ""
|
|
74
|
+
parts << "Failed examples:"
|
|
75
|
+
parts.concat(failure_locations.map(&:strip))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
parts.join("\n")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/aidp/harness/runner.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative "error_handler"
|
|
|
11
11
|
require_relative "status_display"
|
|
12
12
|
require_relative "completion_checker"
|
|
13
13
|
require_relative "../concurrency"
|
|
14
|
+
require_relative "../errors"
|
|
14
15
|
|
|
15
16
|
module Aidp
|
|
16
17
|
module Harness
|
|
@@ -135,6 +136,10 @@ module Aidp
|
|
|
135
136
|
end
|
|
136
137
|
end
|
|
137
138
|
end
|
|
139
|
+
rescue Aidp::Errors::ConfigurationError
|
|
140
|
+
# Configuration errors should crash immediately (crash-early principle)
|
|
141
|
+
# Re-raise without catching
|
|
142
|
+
raise
|
|
138
143
|
rescue => e
|
|
139
144
|
@state = STATES[:error]
|
|
140
145
|
@last_error = e
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require_relative "../tooling_detector"
|
|
5
|
+
require_relative "output_filter"
|
|
5
6
|
|
|
6
7
|
module Aidp
|
|
7
8
|
module Harness
|
|
@@ -11,30 +12,68 @@ module Aidp
|
|
|
11
12
|
def initialize(project_dir, config)
|
|
12
13
|
@project_dir = project_dir
|
|
13
14
|
@config = config
|
|
15
|
+
@iteration_count = 0
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
# Run all configured tests
|
|
17
|
-
# Returns: { success: boolean, output: string, failures: array }
|
|
19
|
+
# Returns: { success: boolean, output: string, failures: array, required_failures: array }
|
|
18
20
|
def run_tests
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
results = test_commands.map { |cmd| execute_command(cmd, "test") }
|
|
23
|
-
aggregate_results(results, "Tests")
|
|
21
|
+
@iteration_count += 1
|
|
22
|
+
run_command_category(:test, "Tests")
|
|
24
23
|
end
|
|
25
24
|
|
|
26
25
|
# Run all configured linters
|
|
27
|
-
# Returns: { success: boolean, output: string, failures: array }
|
|
26
|
+
# Returns: { success: boolean, output: string, failures: array, required_failures: array }
|
|
28
27
|
def run_linters
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
@iteration_count += 1
|
|
29
|
+
run_command_category(:lint, "Linters")
|
|
30
|
+
end
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
# Run all configured formatters
|
|
33
|
+
# Returns: { success: boolean, output: string, failures: array, required_failures: array }
|
|
34
|
+
def run_formatters
|
|
35
|
+
run_command_category(:formatter, "Formatters")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Run all configured build commands
|
|
39
|
+
# Returns: { success: boolean, output: string, failures: array, required_failures: array }
|
|
40
|
+
def run_builds
|
|
41
|
+
run_command_category(:build, "Build")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Run all configured documentation commands
|
|
45
|
+
# Returns: { success: boolean, output: string, failures: array, required_failures: array }
|
|
46
|
+
def run_documentation
|
|
47
|
+
run_command_category(:documentation, "Documentation")
|
|
34
48
|
end
|
|
35
49
|
|
|
36
50
|
private
|
|
37
51
|
|
|
52
|
+
# Run commands for a specific category (test, lint, formatter, build, documentation)
|
|
53
|
+
def run_command_category(category, display_name)
|
|
54
|
+
commands = resolved_commands(category)
|
|
55
|
+
|
|
56
|
+
# If no commands configured, return success (empty check passes)
|
|
57
|
+
return {success: true, output: "", failures: [], required_failures: []} if commands.empty?
|
|
58
|
+
|
|
59
|
+
# Determine output mode based on category
|
|
60
|
+
mode = determine_output_mode(category)
|
|
61
|
+
|
|
62
|
+
# Execute all commands
|
|
63
|
+
results = commands.map do |cmd_config|
|
|
64
|
+
# Handle both string commands (legacy) and hash format (new)
|
|
65
|
+
if cmd_config.is_a?(String)
|
|
66
|
+
result = execute_command(cmd_config, category.to_s)
|
|
67
|
+
result.merge(required: true)
|
|
68
|
+
else
|
|
69
|
+
result = execute_command(cmd_config[:command], category.to_s)
|
|
70
|
+
result.merge(required: cmd_config[:required])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
aggregate_results(results, display_name, mode: mode)
|
|
75
|
+
end
|
|
76
|
+
|
|
38
77
|
def execute_command(command, type)
|
|
39
78
|
stdout, stderr, status = Open3.capture3(command, chdir: @project_dir)
|
|
40
79
|
|
|
@@ -48,53 +87,152 @@ module Aidp
|
|
|
48
87
|
}
|
|
49
88
|
end
|
|
50
89
|
|
|
51
|
-
def aggregate_results(results, category)
|
|
52
|
-
|
|
53
|
-
|
|
90
|
+
def aggregate_results(results, category, mode: :full)
|
|
91
|
+
# Separate required and optional command failures
|
|
92
|
+
all_failures = results.reject { |r| r[:success] }
|
|
93
|
+
required_failures = all_failures.select { |r| r[:required] }
|
|
94
|
+
optional_failures = all_failures.reject { |r| r[:required] }
|
|
95
|
+
|
|
96
|
+
# Success only if all REQUIRED commands pass
|
|
97
|
+
# Optional command failures don't block completion
|
|
98
|
+
success = required_failures.empty?
|
|
54
99
|
|
|
55
|
-
output = if
|
|
56
|
-
"#{category}: All passed"
|
|
100
|
+
output = if all_failures.empty?
|
|
101
|
+
"#{category}: All passed (#{results.length} commands)"
|
|
102
|
+
elsif required_failures.empty?
|
|
103
|
+
"#{category}: Required checks passed (#{optional_failures.length} optional warnings)\n" +
|
|
104
|
+
format_failures(optional_failures, "#{category} - Optional", mode: mode)
|
|
57
105
|
else
|
|
58
|
-
format_failures(
|
|
106
|
+
format_failures(required_failures, "#{category} - Required", mode: mode) +
|
|
107
|
+
(optional_failures.any? ? "\n" + format_failures(optional_failures, "#{category} - Optional", mode: mode) : "")
|
|
59
108
|
end
|
|
60
109
|
|
|
61
110
|
{
|
|
62
111
|
success: success,
|
|
63
112
|
output: output,
|
|
64
|
-
failures:
|
|
113
|
+
failures: all_failures,
|
|
114
|
+
required_failures: required_failures,
|
|
115
|
+
optional_failures: optional_failures
|
|
65
116
|
}
|
|
66
117
|
end
|
|
67
118
|
|
|
68
|
-
def format_failures(failures, category)
|
|
119
|
+
def format_failures(failures, category, mode: :full)
|
|
69
120
|
output = ["#{category} Failures:", ""]
|
|
70
121
|
|
|
71
122
|
failures.each do |failure|
|
|
72
123
|
output << "Command: #{failure[:command]}"
|
|
73
124
|
output << "Exit Code: #{failure[:exit_code]}"
|
|
74
125
|
output << "--- Output ---"
|
|
75
|
-
|
|
76
|
-
|
|
126
|
+
|
|
127
|
+
# Apply filtering based on mode and framework
|
|
128
|
+
filtered_stdout = filter_output(failure[:stdout], mode, detect_framework_from_command(failure[:command]))
|
|
129
|
+
filtered_stderr = filter_output(failure[:stderr], mode, :unknown)
|
|
130
|
+
|
|
131
|
+
output << filtered_stdout unless filtered_stdout.strip.empty?
|
|
132
|
+
output << filtered_stderr unless filtered_stderr.strip.empty?
|
|
77
133
|
output << ""
|
|
78
134
|
end
|
|
79
135
|
|
|
80
136
|
output.join("\n")
|
|
81
137
|
end
|
|
82
138
|
|
|
139
|
+
def filter_output(raw_output, mode, framework)
|
|
140
|
+
return raw_output if mode == :full || raw_output.nil? || raw_output.empty?
|
|
141
|
+
|
|
142
|
+
filter_config = {
|
|
143
|
+
mode: mode,
|
|
144
|
+
include_context: true,
|
|
145
|
+
context_lines: 3,
|
|
146
|
+
max_lines: 500
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
filter = OutputFilter.new(filter_config)
|
|
150
|
+
filter.filter(raw_output, framework: framework)
|
|
151
|
+
rescue NameError
|
|
152
|
+
# Logging infrastructure not available
|
|
153
|
+
raw_output
|
|
154
|
+
rescue => e
|
|
155
|
+
Aidp.log_warn("test_runner", "filter_failed",
|
|
156
|
+
error: e.message,
|
|
157
|
+
framework: framework)
|
|
158
|
+
raw_output # Fallback to unfiltered on error
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def detect_framework_from_command(command)
|
|
162
|
+
case command
|
|
163
|
+
when /rspec/
|
|
164
|
+
:rspec
|
|
165
|
+
when /minitest/
|
|
166
|
+
:minitest
|
|
167
|
+
when /jest/
|
|
168
|
+
:jest
|
|
169
|
+
when /pytest/
|
|
170
|
+
:pytest
|
|
171
|
+
else
|
|
172
|
+
:unknown
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def determine_output_mode(category)
|
|
177
|
+
# Check config for category-specific mode
|
|
178
|
+
case category
|
|
179
|
+
when :test
|
|
180
|
+
if @config.respond_to?(:test_output_mode)
|
|
181
|
+
@config.test_output_mode
|
|
182
|
+
elsif @iteration_count > 1
|
|
183
|
+
:failures_only
|
|
184
|
+
else
|
|
185
|
+
:full
|
|
186
|
+
end
|
|
187
|
+
when :lint
|
|
188
|
+
if @config.respond_to?(:lint_output_mode)
|
|
189
|
+
@config.lint_output_mode
|
|
190
|
+
elsif @iteration_count > 1
|
|
191
|
+
:failures_only
|
|
192
|
+
else
|
|
193
|
+
:full
|
|
194
|
+
end
|
|
195
|
+
else
|
|
196
|
+
:full
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Resolve commands for a specific category
|
|
201
|
+
# Returns normalized command configs (array of {command:, required:} hashes)
|
|
202
|
+
def resolved_commands(category)
|
|
203
|
+
case category
|
|
204
|
+
when :test
|
|
205
|
+
resolved_test_commands
|
|
206
|
+
when :lint
|
|
207
|
+
resolved_lint_commands
|
|
208
|
+
when :formatter
|
|
209
|
+
@config.formatter_commands
|
|
210
|
+
when :build
|
|
211
|
+
@config.build_commands
|
|
212
|
+
when :documentation
|
|
213
|
+
@config.documentation_commands
|
|
214
|
+
else
|
|
215
|
+
[]
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
83
219
|
def resolved_test_commands
|
|
84
|
-
explicit =
|
|
220
|
+
explicit = @config.test_commands
|
|
85
221
|
return explicit unless explicit.empty?
|
|
86
222
|
|
|
87
|
-
|
|
88
|
-
|
|
223
|
+
# Auto-detect test commands if none explicitly configured
|
|
224
|
+
detected = detected_tooling.test_commands.map { |cmd| {command: cmd, required: true} }
|
|
225
|
+
log_fallback(:tests, detected.map { |c| c[:command] }) unless detected.empty?
|
|
89
226
|
detected
|
|
90
227
|
end
|
|
91
228
|
|
|
92
229
|
def resolved_lint_commands
|
|
93
|
-
explicit =
|
|
230
|
+
explicit = @config.lint_commands
|
|
94
231
|
return explicit unless explicit.empty?
|
|
95
232
|
|
|
96
|
-
|
|
97
|
-
|
|
233
|
+
# Auto-detect lint commands if none explicitly configured
|
|
234
|
+
detected = detected_tooling.lint_commands.map { |cmd| {command: cmd, required: true} }
|
|
235
|
+
log_fallback(:linters, detected.map { |c| c[:command] }) unless detected.empty?
|
|
98
236
|
detected
|
|
99
237
|
end
|
|
100
238
|
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "capability_registry"
|
|
4
4
|
require_relative "configuration"
|
|
5
|
+
require_relative "../message_display"
|
|
5
6
|
|
|
6
7
|
module Aidp
|
|
7
8
|
module Harness
|
|
8
9
|
# Manages thinking depth tier selection and escalation
|
|
9
10
|
# Integrates with CapabilityRegistry and Configuration to select appropriate models
|
|
10
11
|
class ThinkingDepthManager
|
|
12
|
+
include Aidp::MessageDisplay
|
|
13
|
+
|
|
11
14
|
attr_reader :configuration, :registry
|
|
12
15
|
|
|
13
16
|
def initialize(configuration, registry: nil, root_dir: nil)
|
|
@@ -150,11 +153,48 @@ module Aidp
|
|
|
150
153
|
tier ||= current_tier
|
|
151
154
|
validate_tier!(tier)
|
|
152
155
|
|
|
153
|
-
#
|
|
156
|
+
# First, try to get models from user's configuration for this tier
|
|
157
|
+
configured_models = configuration.models_for_tier(tier)
|
|
158
|
+
|
|
159
|
+
if configured_models.any?
|
|
160
|
+
# If provider specified, try to find model for that provider in config
|
|
161
|
+
if provider
|
|
162
|
+
matching_model = configured_models.find { |m| m[:provider] == provider }
|
|
163
|
+
if matching_model
|
|
164
|
+
Aidp.log_debug("thinking_depth_manager", "Selected model from user config",
|
|
165
|
+
tier: tier,
|
|
166
|
+
provider: provider,
|
|
167
|
+
model: matching_model[:model])
|
|
168
|
+
return [matching_model[:provider], matching_model[:model], {}]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# If provider doesn't support tier and switching allowed, try other providers in config
|
|
172
|
+
unless configuration.allow_provider_switch_for_tier?
|
|
173
|
+
Aidp.log_warn("thinking_depth_manager", "Provider lacks tier in config, switching disabled",
|
|
174
|
+
tier: tier,
|
|
175
|
+
provider: provider)
|
|
176
|
+
return nil
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Try any configured model for this tier (prioritize first in list)
|
|
181
|
+
first_model = configured_models.first
|
|
182
|
+
if first_model
|
|
183
|
+
Aidp.log_info("thinking_depth_manager", "Selected model from user config",
|
|
184
|
+
tier: tier,
|
|
185
|
+
original_provider: provider,
|
|
186
|
+
selected_provider: first_model[:provider],
|
|
187
|
+
model: first_model[:model])
|
|
188
|
+
return [first_model[:provider], first_model[:model], {}]
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Fall back to catalog-based selection if no models in user config
|
|
193
|
+
# If provider specified, try to find model for that provider in catalog
|
|
154
194
|
if provider
|
|
155
195
|
model_name, model_data = @registry.best_model_for_tier(tier, provider)
|
|
156
196
|
if model_name
|
|
157
|
-
Aidp.log_debug("thinking_depth_manager", "Selected model",
|
|
197
|
+
Aidp.log_debug("thinking_depth_manager", "Selected model from catalog",
|
|
158
198
|
tier: tier,
|
|
159
199
|
provider: provider,
|
|
160
200
|
model: model_name)
|
|
@@ -163,20 +203,24 @@ module Aidp
|
|
|
163
203
|
|
|
164
204
|
# If provider doesn't support tier and switching allowed, try others
|
|
165
205
|
unless configuration.allow_provider_switch_for_tier?
|
|
166
|
-
Aidp.log_warn("thinking_depth_manager", "Provider lacks tier, switching disabled",
|
|
206
|
+
Aidp.log_warn("thinking_depth_manager", "Provider lacks tier in catalog, switching disabled",
|
|
167
207
|
tier: tier,
|
|
168
208
|
provider: provider)
|
|
169
209
|
return nil
|
|
170
210
|
end
|
|
171
211
|
end
|
|
172
212
|
|
|
173
|
-
# Try all providers
|
|
213
|
+
# Try all providers in catalog
|
|
214
|
+
if provider && !configuration.allow_provider_switch_for_tier?
|
|
215
|
+
return nil
|
|
216
|
+
end
|
|
217
|
+
|
|
174
218
|
providers_to_try = provider ? [@registry.provider_names - [provider]].flatten : @registry.provider_names
|
|
175
219
|
|
|
176
220
|
providers_to_try.each do |prov_name|
|
|
177
221
|
model_name, model_data = @registry.best_model_for_tier(tier, prov_name)
|
|
178
222
|
if model_name
|
|
179
|
-
Aidp.log_info("thinking_depth_manager", "Selected model from alternate provider",
|
|
223
|
+
Aidp.log_info("thinking_depth_manager", "Selected model from catalog (alternate provider)",
|
|
180
224
|
tier: tier,
|
|
181
225
|
original_provider: provider,
|
|
182
226
|
selected_provider: prov_name,
|
|
@@ -185,10 +229,23 @@ module Aidp
|
|
|
185
229
|
end
|
|
186
230
|
end
|
|
187
231
|
|
|
188
|
-
|
|
232
|
+
# No model found for requested tier - try fallback to other tiers
|
|
233
|
+
Aidp.log_warn("thinking_depth_manager", "No model found for requested tier, trying fallback",
|
|
189
234
|
tier: tier,
|
|
190
235
|
provider: provider)
|
|
191
|
-
|
|
236
|
+
|
|
237
|
+
result = try_fallback_tiers(tier, provider)
|
|
238
|
+
|
|
239
|
+
unless result
|
|
240
|
+
# Enhanced error message with discovery hints
|
|
241
|
+
display_enhanced_tier_error(tier, provider)
|
|
242
|
+
|
|
243
|
+
Aidp.log_error("thinking_depth_manager", "No model found for tier or fallback tiers",
|
|
244
|
+
tier: tier,
|
|
245
|
+
provider: provider)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
result
|
|
192
249
|
end
|
|
193
250
|
|
|
194
251
|
# Get tier for a specific model
|
|
@@ -330,6 +387,165 @@ module Aidp
|
|
|
330
387
|
# Keep history bounded
|
|
331
388
|
@tier_history.shift if @tier_history.size > 100
|
|
332
389
|
end
|
|
390
|
+
|
|
391
|
+
# Try to find a model in fallback tiers when requested tier has no models
|
|
392
|
+
# Tries lower tiers first (cheaper), then higher tiers
|
|
393
|
+
# Returns [provider_name, model_name, model_data] or nil
|
|
394
|
+
def try_fallback_tiers(requested_tier, provider)
|
|
395
|
+
# Generate fallback order: try lower tiers first, then higher
|
|
396
|
+
fallback_tiers = generate_fallback_tier_order(requested_tier)
|
|
397
|
+
|
|
398
|
+
fallback_tiers.each do |fallback_tier|
|
|
399
|
+
# First, try user's configuration for this fallback tier
|
|
400
|
+
configured_models = configuration.models_for_tier(fallback_tier)
|
|
401
|
+
|
|
402
|
+
if configured_models.any?
|
|
403
|
+
# Try specified provider first if given
|
|
404
|
+
if provider
|
|
405
|
+
matching_model = configured_models.find { |m| m[:provider] == provider }
|
|
406
|
+
if matching_model
|
|
407
|
+
Aidp.log_warn("thinking_depth_manager", "Falling back to different tier (from config)",
|
|
408
|
+
requested_tier: requested_tier,
|
|
409
|
+
fallback_tier: fallback_tier,
|
|
410
|
+
provider: provider,
|
|
411
|
+
model: matching_model[:model])
|
|
412
|
+
return [matching_model[:provider], matching_model[:model], {}]
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Try any configured model for this tier
|
|
417
|
+
first_model = configured_models.first
|
|
418
|
+
if first_model
|
|
419
|
+
Aidp.log_warn("thinking_depth_manager", "Falling back to different tier and provider (from config)",
|
|
420
|
+
requested_tier: requested_tier,
|
|
421
|
+
fallback_tier: fallback_tier,
|
|
422
|
+
requested_provider: provider,
|
|
423
|
+
fallback_provider: first_model[:provider],
|
|
424
|
+
model: first_model[:model])
|
|
425
|
+
return [first_model[:provider], first_model[:model], {}]
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Fall back to catalog if no models in config
|
|
430
|
+
# Try specified provider first if given
|
|
431
|
+
if provider
|
|
432
|
+
model_name, model_data = @registry.best_model_for_tier(fallback_tier, provider)
|
|
433
|
+
if model_name
|
|
434
|
+
Aidp.log_warn("thinking_depth_manager", "Falling back to different tier (from catalog)",
|
|
435
|
+
requested_tier: requested_tier,
|
|
436
|
+
fallback_tier: fallback_tier,
|
|
437
|
+
provider: provider,
|
|
438
|
+
model: model_name)
|
|
439
|
+
return [provider, model_name, model_data]
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Try all available providers in catalog
|
|
444
|
+
@registry.provider_names.each do |prov_name|
|
|
445
|
+
next if prov_name == provider # Skip if already tried above
|
|
446
|
+
|
|
447
|
+
model_name, model_data = @registry.best_model_for_tier(fallback_tier, prov_name)
|
|
448
|
+
if model_name
|
|
449
|
+
Aidp.log_warn("thinking_depth_manager", "Falling back to different tier and provider (from catalog)",
|
|
450
|
+
requested_tier: requested_tier,
|
|
451
|
+
fallback_tier: fallback_tier,
|
|
452
|
+
requested_provider: provider,
|
|
453
|
+
fallback_provider: prov_name,
|
|
454
|
+
model: model_name)
|
|
455
|
+
return [prov_name, model_name, model_data]
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
nil
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Generate fallback tier order: lower tiers first (cheaper), then higher
|
|
464
|
+
# For example, if tier is "standard", try: mini, thinking, pro, max
|
|
465
|
+
def generate_fallback_tier_order(tier)
|
|
466
|
+
current_priority = @registry.tier_priority(tier) || 1
|
|
467
|
+
all_tiers = CapabilityRegistry::VALID_TIERS
|
|
468
|
+
|
|
469
|
+
# Split into lower and higher tiers
|
|
470
|
+
lower_tiers = all_tiers.select { |t| (@registry.tier_priority(t) || 0) < current_priority }.reverse
|
|
471
|
+
higher_tiers = all_tiers.select { |t| (@registry.tier_priority(t) || 0) > current_priority }
|
|
472
|
+
|
|
473
|
+
# Try lower tiers first (cost optimization), then higher tiers
|
|
474
|
+
lower_tiers + higher_tiers
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Display enhanced error message with discovery hints
|
|
478
|
+
def display_enhanced_tier_error(tier, provider)
|
|
479
|
+
return unless defined?(Aidp::MessageDisplay)
|
|
480
|
+
|
|
481
|
+
# Check if there are discovered models in cache
|
|
482
|
+
discovered_models = check_discovered_models(tier, provider)
|
|
483
|
+
|
|
484
|
+
if discovered_models&.any?
|
|
485
|
+
display_tier_error_with_suggestions(tier, provider, discovered_models)
|
|
486
|
+
else
|
|
487
|
+
display_tier_error_with_discovery_hint(tier, provider)
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Check cache for discovered models for this tier
|
|
492
|
+
def check_discovered_models(tier, provider)
|
|
493
|
+
require_relative "model_cache"
|
|
494
|
+
require_relative "model_registry"
|
|
495
|
+
|
|
496
|
+
cache = Aidp::Harness::ModelCache.new
|
|
497
|
+
registry = Aidp::Harness::ModelRegistry.new
|
|
498
|
+
|
|
499
|
+
# Get all cached models for the provider
|
|
500
|
+
cached_models = cache.get_cached_models(provider)
|
|
501
|
+
return nil unless cached_models&.any?
|
|
502
|
+
|
|
503
|
+
# Filter to models for the requested tier
|
|
504
|
+
tier_models = cached_models.select do |model|
|
|
505
|
+
family = model[:family] || model["family"]
|
|
506
|
+
model_info = registry.get_model_info(family)
|
|
507
|
+
model_info && model_info["tier"] == tier.to_s
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
tier_models.any? ? tier_models : nil
|
|
511
|
+
rescue => e
|
|
512
|
+
Aidp.log_debug("thinking_depth_manager", "failed to check cached models",
|
|
513
|
+
error: e.message)
|
|
514
|
+
nil
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# Display error with model suggestions from cache
|
|
518
|
+
def display_tier_error_with_suggestions(tier, provider, models)
|
|
519
|
+
display_message("\nā No model configured for '#{tier}' tier", type: :error)
|
|
520
|
+
display_message(" Provider: #{provider}", type: :info) if provider
|
|
521
|
+
|
|
522
|
+
display_message("\nš” Discovered models for this tier:", type: :highlight)
|
|
523
|
+
models.first(3).each do |model|
|
|
524
|
+
model_name = model[:name] || model["name"]
|
|
525
|
+
display_message(" - #{model_name}", type: :info)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
display_message("\n Add to aidp.yml:", type: :highlight)
|
|
529
|
+
display_message(" providers:", type: :info)
|
|
530
|
+
display_message(" #{provider}:", type: :info)
|
|
531
|
+
display_message(" thinking:", type: :info)
|
|
532
|
+
display_message(" tiers:", type: :info)
|
|
533
|
+
display_message(" #{tier}:", type: :info)
|
|
534
|
+
display_message(" models:", type: :info)
|
|
535
|
+
first_model = models.first[:name] || models.first["name"]
|
|
536
|
+
display_message(" - model: #{first_model}\n", type: :info)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Display error with discovery hint
|
|
540
|
+
def display_tier_error_with_discovery_hint(tier, provider)
|
|
541
|
+
display_message("\nā No model configured for '#{tier}' tier", type: :error)
|
|
542
|
+
display_message(" Provider: #{provider}", type: :info) if provider
|
|
543
|
+
|
|
544
|
+
display_message("\nš” Suggested actions:", type: :highlight)
|
|
545
|
+
display_message(" 1. Run 'aidp models discover' to find available models", type: :info)
|
|
546
|
+
display_message(" 2. Run 'aidp models list --tier=#{tier}' to see models for this tier", type: :info)
|
|
547
|
+
display_message(" 3. Run 'aidp models validate' to check your configuration\n", type: :info)
|
|
548
|
+
end
|
|
333
549
|
end
|
|
334
550
|
end
|
|
335
551
|
end
|
|
@@ -23,13 +23,16 @@ module Aidp
|
|
|
23
23
|
def initialize(prompt: TTY::Prompt.new, tty: $stdin)
|
|
24
24
|
@cursor = TTY::Cursor
|
|
25
25
|
@screen = TTY::Screen
|
|
26
|
-
@pastel = Pastel.new
|
|
27
26
|
@prompt = prompt
|
|
28
27
|
|
|
29
28
|
# Headless (non-interactive) detection for test/CI environments:
|
|
30
29
|
# - STDIN not a TTY (captured by PTY/tmux harness or test environment)
|
|
31
30
|
@headless = !!(tty.nil? || !tty.tty?)
|
|
32
31
|
|
|
32
|
+
# Initialize Pastel with disabled colors in headless mode to avoid
|
|
33
|
+
# "closed stream" errors when checking TTY capabilities
|
|
34
|
+
@pastel = Pastel.new(enabled: !@headless)
|
|
35
|
+
|
|
33
36
|
@current_mode = nil
|
|
34
37
|
@workflow_active = false
|
|
35
38
|
@current_step = nil
|