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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -6
  3. data/lib/aidp/analyze/error_handler.rb +11 -0
  4. data/lib/aidp/cli/checkpoint_command.rb +198 -0
  5. data/lib/aidp/cli/config_command.rb +71 -0
  6. data/lib/aidp/cli/enhanced_input.rb +2 -0
  7. data/lib/aidp/cli/first_run_wizard.rb +8 -7
  8. data/lib/aidp/cli/harness_command.rb +102 -0
  9. data/lib/aidp/cli/jobs_command.rb +3 -3
  10. data/lib/aidp/cli/mcp_dashboard.rb +4 -3
  11. data/lib/aidp/cli/models_command.rb +662 -0
  12. data/lib/aidp/cli/providers_command.rb +223 -0
  13. data/lib/aidp/cli.rb +35 -456
  14. data/lib/aidp/daemon/runner.rb +2 -2
  15. data/lib/aidp/debug_mixin.rb +2 -9
  16. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  17. data/lib/aidp/execute/checkpoint_display.rb +38 -37
  18. data/lib/aidp/execute/interactive_repl.rb +2 -1
  19. data/lib/aidp/execute/prompt_manager.rb +4 -4
  20. data/lib/aidp/execute/work_loop_runner.rb +253 -56
  21. data/lib/aidp/execute/workflow_selector.rb +2 -2
  22. data/lib/aidp/harness/config_loader.rb +20 -11
  23. data/lib/aidp/harness/config_manager.rb +5 -5
  24. data/lib/aidp/harness/config_schema.rb +30 -8
  25. data/lib/aidp/harness/configuration.rb +105 -4
  26. data/lib/aidp/harness/enhanced_runner.rb +24 -15
  27. data/lib/aidp/harness/error_handler.rb +26 -5
  28. data/lib/aidp/harness/filter_strategy.rb +45 -0
  29. data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
  30. data/lib/aidp/harness/model_cache.rb +269 -0
  31. data/lib/aidp/harness/model_discovery_service.rb +259 -0
  32. data/lib/aidp/harness/model_registry.rb +201 -0
  33. data/lib/aidp/harness/output_filter.rb +136 -0
  34. data/lib/aidp/harness/provider_manager.rb +18 -3
  35. data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
  36. data/lib/aidp/harness/runner.rb +5 -0
  37. data/lib/aidp/harness/test_runner.rb +165 -27
  38. data/lib/aidp/harness/thinking_depth_manager.rb +223 -7
  39. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
  40. data/lib/aidp/logger.rb +35 -5
  41. data/lib/aidp/providers/adapter.rb +2 -4
  42. data/lib/aidp/providers/anthropic.rb +141 -128
  43. data/lib/aidp/providers/base.rb +98 -2
  44. data/lib/aidp/providers/capability_registry.rb +0 -1
  45. data/lib/aidp/providers/codex.rb +49 -67
  46. data/lib/aidp/providers/cursor.rb +71 -59
  47. data/lib/aidp/providers/gemini.rb +44 -60
  48. data/lib/aidp/providers/github_copilot.rb +2 -66
  49. data/lib/aidp/providers/kilocode.rb +24 -80
  50. data/lib/aidp/providers/opencode.rb +24 -80
  51. data/lib/aidp/safe_directory.rb +10 -3
  52. data/lib/aidp/setup/wizard.rb +345 -8
  53. data/lib/aidp/storage/csv_storage.rb +9 -3
  54. data/lib/aidp/storage/file_manager.rb +8 -2
  55. data/lib/aidp/storage/json_storage.rb +9 -3
  56. data/lib/aidp/version.rb +1 -1
  57. data/lib/aidp/watch/build_processor.rb +40 -1
  58. data/lib/aidp/watch/change_request_processor.rb +659 -0
  59. data/lib/aidp/watch/plan_generator.rb +93 -14
  60. data/lib/aidp/watch/plan_processor.rb +71 -8
  61. data/lib/aidp/watch/repository_client.rb +85 -20
  62. data/lib/aidp/watch/review_processor.rb +3 -3
  63. data/lib/aidp/watch/runner.rb +37 -0
  64. data/lib/aidp/watch/state_store.rb +46 -1
  65. data/lib/aidp/workflows/guided_agent.rb +3 -3
  66. data/lib/aidp/workstream_executor.rb +5 -2
  67. data/lib/aidp.rb +4 -0
  68. data/templates/aidp-development.yml.example +2 -2
  69. data/templates/aidp-production.yml.example +3 -3
  70. data/templates/aidp.yml.example +53 -0
  71. metadata +14 -1
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model_cache"
4
+ require_relative "model_registry"
5
+
6
+ module Aidp
7
+ module Harness
8
+ # Service for discovering available models from providers
9
+ #
10
+ # Orchestrates model discovery across multiple providers:
11
+ # 1. Checks cache first (with TTL)
12
+ # 2. Falls back to dynamic discovery via provider.discover_models
13
+ # 3. Merges with static registry for comprehensive results
14
+ # 4. Caches results for future use
15
+ #
16
+ # Usage:
17
+ # service = ModelDiscoveryService.new
18
+ # models = service.discover_models("anthropic")
19
+ # all_models = service.discover_all_models
20
+ class ModelDiscoveryService
21
+ attr_reader :cache, :registry
22
+
23
+ def initialize(cache: nil, registry: nil)
24
+ @cache = cache || ModelCache.new
25
+ @registry = registry || ModelRegistry.new
26
+ @provider_classes = discover_provider_classes
27
+ Aidp.log_debug("model_discovery_service", "initialized",
28
+ providers: @provider_classes.keys)
29
+ end
30
+
31
+ # Discover models for a specific provider
32
+ #
33
+ # @param provider [String] Provider name (e.g., "anthropic", "cursor")
34
+ # @param use_cache [Boolean] Whether to use cached results (default: true)
35
+ # @return [Array<Hash>] Discovered models
36
+ def discover_models(provider, use_cache: true)
37
+ Aidp.log_info("model_discovery_service", "discovering models",
38
+ provider: provider, use_cache: use_cache)
39
+
40
+ # Check cache first
41
+ if use_cache
42
+ cached = @cache.get_cached_models(provider)
43
+ if cached
44
+ Aidp.log_debug("model_discovery_service", "using cached models",
45
+ provider: provider, count: cached.size)
46
+ return cached
47
+ end
48
+ end
49
+
50
+ # Perform discovery
51
+ models = perform_discovery(provider)
52
+
53
+ # Cache the results
54
+ @cache.cache_models(provider, models) if models.any?
55
+
56
+ models
57
+ rescue => e
58
+ Aidp.log_error("model_discovery_service", "discovery failed",
59
+ provider: provider, error: e.message, backtrace: e.backtrace.first(3))
60
+ []
61
+ end
62
+
63
+ # Discover models from all available providers
64
+ #
65
+ # @param use_cache [Boolean] Whether to use cached results
66
+ # @return [Hash] Hash of provider => models array
67
+ def discover_all_models(use_cache: true)
68
+ results = {}
69
+
70
+ @provider_classes.each_key do |provider|
71
+ models = discover_models(provider, use_cache: use_cache)
72
+ results[provider] = models if models.any?
73
+ end
74
+
75
+ Aidp.log_info("model_discovery_service", "discovered all models",
76
+ providers: results.keys, total_models: results.values.flatten.size)
77
+ results
78
+ end
79
+
80
+ # Discover models concurrently from multiple providers
81
+ #
82
+ # @param providers [Array<String>] List of provider names
83
+ # @param use_cache [Boolean] Whether to use cached results
84
+ # @return [Hash] Hash of provider => models array
85
+ def discover_concurrent(providers, use_cache: true)
86
+ require "concurrent"
87
+
88
+ results = {}
89
+ mutex = Mutex.new
90
+
91
+ # Create a thread pool
92
+ pool = Concurrent::FixedThreadPool.new(providers.size)
93
+
94
+ # Submit discovery tasks
95
+ futures = providers.map do |provider|
96
+ Concurrent::Future.execute(executor: pool) do
97
+ models = discover_models(provider, use_cache: use_cache)
98
+ mutex.synchronize { results[provider] = models }
99
+ end
100
+ end
101
+
102
+ # Wait for all to complete
103
+ futures.each(&:wait)
104
+
105
+ pool.shutdown
106
+ pool.wait_for_termination(30)
107
+
108
+ Aidp.log_info("model_discovery_service", "concurrent discovery complete",
109
+ providers: results.keys, total_models: results.values.flatten.size)
110
+ results
111
+ rescue LoadError => e
112
+ # Fallback to sequential if concurrent gem not available
113
+ Aidp.log_warn("model_discovery_service", "concurrent gem not available, using sequential",
114
+ error: e.message)
115
+ providers.each_with_object({}) do |provider, hash|
116
+ hash[provider] = discover_models(provider, use_cache: use_cache)
117
+ end
118
+ rescue => e
119
+ Aidp.log_error("model_discovery_service", "concurrent discovery failed",
120
+ error: e.message)
121
+ {}
122
+ end
123
+
124
+ # Get all available models (discovery + static registry)
125
+ #
126
+ # Combines dynamically discovered models with static registry
127
+ #
128
+ # @param use_cache [Boolean] Whether to use cached results
129
+ # @return [Hash] Hash with :discovered and :registry keys
130
+ def all_available_models(use_cache: true)
131
+ discovered = discover_all_models(use_cache: use_cache)
132
+ registry_families = @registry.all_families
133
+
134
+ {
135
+ discovered: discovered,
136
+ registry: registry_families.map { |family| @registry.get_model_info(family) }.compact
137
+ }
138
+ end
139
+
140
+ # Find which providers support a given model family
141
+ #
142
+ # @param family_name [String] Model family name
143
+ # @return [Array<String>] List of provider names
144
+ def providers_supporting(family_name)
145
+ providers = []
146
+
147
+ @provider_classes.each do |provider_name, class_name|
148
+ provider_class = constantize_provider(class_name)
149
+ next unless provider_class
150
+
151
+ if provider_class.respond_to?(:supports_model_family?)
152
+ providers << provider_name if provider_class.supports_model_family?(family_name)
153
+ end
154
+ end
155
+
156
+ providers
157
+ end
158
+
159
+ # Refresh cache for all providers
160
+ def refresh_all_caches
161
+ @cache.invalidate_all
162
+ discover_all_models(use_cache: false)
163
+ end
164
+
165
+ # Refresh cache for specific provider
166
+ #
167
+ # @param provider [String] Provider name
168
+ def refresh_cache(provider)
169
+ @cache.invalidate(provider)
170
+ discover_models(provider, use_cache: false)
171
+ end
172
+
173
+ private
174
+
175
+ def perform_discovery(provider)
176
+ provider_class = get_provider_class(provider)
177
+ unless provider_class
178
+ Aidp.log_warn("model_discovery_service", "unknown provider",
179
+ provider: provider)
180
+ return []
181
+ end
182
+
183
+ unless provider_class.respond_to?(:available?) && provider_class.available?
184
+ Aidp.log_debug("model_discovery_service", "provider not available",
185
+ provider: provider)
186
+ return []
187
+ end
188
+
189
+ unless provider_class.respond_to?(:discover_models)
190
+ Aidp.log_warn("model_discovery_service", "provider missing discover_models",
191
+ provider: provider)
192
+ return []
193
+ end
194
+
195
+ models = provider_class.discover_models
196
+ Aidp.log_info("model_discovery_service", "discovered models",
197
+ provider: provider, count: models.size)
198
+ models
199
+ end
200
+
201
+ def get_provider_class(provider)
202
+ class_name = @provider_classes[provider]
203
+ return nil unless class_name
204
+
205
+ constantize_provider(class_name)
206
+ end
207
+
208
+ def constantize_provider(class_name)
209
+ # Safely constantize the provider class
210
+ parts = class_name.split("::")
211
+ parts.reduce(Object) { |mod, name| mod.const_get(name) }
212
+ rescue NameError => e
213
+ Aidp.log_debug("model_discovery_service", "provider class not found",
214
+ class: class_name, error: e.message)
215
+ nil
216
+ end
217
+
218
+ # Dynamically discover all provider classes from the providers directory
219
+ #
220
+ # @return [Hash] Hash of provider_name => class_name
221
+ def discover_provider_classes
222
+ providers_dir = File.join(__dir__, "../providers")
223
+ provider_files = Dir.glob("*.rb", base: providers_dir)
224
+
225
+ # Exclude base classes and utility files
226
+ excluded_files = ["base.rb", "adapter.rb", "error_taxonomy.rb", "capability_registry.rb"]
227
+ provider_files -= excluded_files
228
+
229
+ providers = {}
230
+
231
+ provider_files.each do |file|
232
+ provider_name = File.basename(file, ".rb")
233
+ # Convert to class name (e.g., "anthropic" -> "Anthropic", "github_copilot" -> "GithubCopilot")
234
+ class_name = provider_name.split("_").map(&:capitalize).join
235
+ full_class_name = "Aidp::Providers::#{class_name}"
236
+
237
+ # Try to load and verify the provider class exists
238
+ begin
239
+ require_relative "../providers/#{provider_name}"
240
+ provider_class = constantize_provider(full_class_name)
241
+ if provider_class&.respond_to?(:discover_models)
242
+ providers[provider_name] = full_class_name
243
+ end
244
+ rescue => e
245
+ # Skip providers that can't be loaded or don't implement discover_models
246
+ if ENV["DEBUG"]
247
+ Aidp.log_debug("model_discovery_service", "skipping provider",
248
+ provider: provider_name, reason: e.message)
249
+ end
250
+ end
251
+ end
252
+
253
+ Aidp.log_debug("model_discovery_service", "discovered provider classes",
254
+ count: providers.size, providers: providers.keys)
255
+ providers
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "pathname"
5
+
6
+ module Aidp
7
+ module Harness
8
+ # ModelRegistry manages the static registry of known model families and their tier classifications
9
+ #
10
+ # The registry uses model families (e.g., "claude-3-5-sonnet") rather than specific versioned
11
+ # model IDs (e.g., "claude-3-5-sonnet-20241022"). This design provides:
12
+ # - No version tracking burden - registry tracks families, not every dated version
13
+ # - Future-proofing - new model versions automatically inherit family tier
14
+ # - Provider autonomy - each provider handles version-specific naming
15
+ #
16
+ # Usage:
17
+ # registry = ModelRegistry.new
18
+ # registry.get_model_info("claude-3-5-sonnet")
19
+ # # => { name: "Claude 3.5 Sonnet", tier: "standard", ... }
20
+ #
21
+ # registry.models_for_tier("standard")
22
+ # # => ["claude-3-5-sonnet", "gpt-4-turbo", ...]
23
+ #
24
+ # registry.match_to_family("claude-3-5-sonnet-20241022")
25
+ # # => "claude-3-5-sonnet"
26
+ class ModelRegistry
27
+ class RegistryError < StandardError; end
28
+ class InvalidRegistrySchema < RegistryError; end
29
+ class ModelNotFound < RegistryError; end
30
+
31
+ VALID_TIERS = %w[mini standard advanced].freeze
32
+ VALID_CAPABILITIES = %w[chat code vision tool_use streaming json_mode].freeze
33
+ VALID_SPEEDS = %w[very_fast fast medium slow].freeze
34
+
35
+ attr_reader :registry_data
36
+
37
+ def initialize(registry_path: nil)
38
+ @registry_path = registry_path || default_registry_path
39
+ @registry_data = load_static_registry
40
+ validate_registry_schema
41
+ Aidp.log_debug("model_registry", "initialized", models: @registry_data["model_families"].keys.size)
42
+ end
43
+
44
+ # Get complete model information for a family
45
+ #
46
+ # @param family_name [String] The model family name (e.g., "claude-3-5-sonnet")
47
+ # @return [Hash, nil] Model metadata hash or nil if not found
48
+ def get_model_info(family_name)
49
+ info = @registry_data["model_families"][family_name]
50
+ return nil unless info
51
+
52
+ info.merge("family" => family_name)
53
+ end
54
+
55
+ # Get all model families for a specific tier
56
+ #
57
+ # @param tier [String, Symbol] The tier name (mini, standard, advanced)
58
+ # @return [Array<String>] List of model family names for the tier
59
+ def models_for_tier(tier)
60
+ tier_str = tier.to_s
61
+ unless VALID_TIERS.include?(tier_str)
62
+ Aidp.log_warn("model_registry", "invalid tier requested", tier: tier_str, valid: VALID_TIERS)
63
+ return []
64
+ end
65
+
66
+ families = @registry_data["model_families"].select { |_family, info|
67
+ info["tier"] == tier_str
68
+ }.keys
69
+
70
+ Aidp.log_debug("model_registry", "found models for tier", tier: tier_str, count: families.size)
71
+ families
72
+ end
73
+
74
+ # Classify a model family's tier
75
+ #
76
+ # @param family_name [String] The model family name
77
+ # @return [String, nil] The tier name or nil if not found
78
+ def classify_model_tier(family_name)
79
+ info = get_model_info(family_name)
80
+ tier = info&.fetch("tier", nil)
81
+ Aidp.log_debug("model_registry", "classified tier", family: family_name, tier: tier)
82
+ tier
83
+ end
84
+
85
+ # Match a versioned model name to its family using pattern matching
86
+ #
87
+ # This method attempts to normalize versioned model names (e.g., "claude-3-5-sonnet-20241022")
88
+ # to their family name (e.g., "claude-3-5-sonnet") by testing against version_pattern regexes.
89
+ #
90
+ # @param versioned_name [String] The versioned model name
91
+ # @return [String, nil] The family name if matched, nil otherwise
92
+ def match_to_family(versioned_name)
93
+ @registry_data["model_families"].each do |family, info|
94
+ pattern = info["version_pattern"]
95
+ next unless pattern
96
+
97
+ begin
98
+ regex = Regexp.new("^#{pattern}$")
99
+ if regex.match?(versioned_name)
100
+ Aidp.log_debug("model_registry", "matched to family", versioned: versioned_name, family: family)
101
+ return family
102
+ end
103
+ rescue RegexpError => e
104
+ Aidp.log_error("model_registry", "invalid pattern", family: family, pattern: pattern, error: e.message)
105
+ end
106
+ end
107
+
108
+ # If no pattern matches, check if the versioned_name is itself a family
109
+ if @registry_data["model_families"].key?(versioned_name)
110
+ Aidp.log_debug("model_registry", "exact family match", name: versioned_name)
111
+ return versioned_name
112
+ end
113
+
114
+ Aidp.log_debug("model_registry", "no family match", versioned: versioned_name)
115
+ nil
116
+ end
117
+
118
+ # Get all registered model families
119
+ #
120
+ # @return [Array<String>] List of all model family names
121
+ def all_families
122
+ @registry_data["model_families"].keys
123
+ end
124
+
125
+ # Check if a model family exists in the registry
126
+ #
127
+ # @param family_name [String] The model family name
128
+ # @return [Boolean] True if the family exists
129
+ def family_exists?(family_name)
130
+ @registry_data["model_families"].key?(family_name)
131
+ end
132
+
133
+ # Get all tiers that have at least one model
134
+ #
135
+ # @return [Array<String>] List of tier names
136
+ def available_tiers
137
+ @registry_data["model_families"].values.map { |info| info["tier"] }.uniq.sort
138
+ end
139
+
140
+ private
141
+
142
+ def default_registry_path
143
+ Pathname.new(__dir__).parent.join("data", "model_registry.yml")
144
+ end
145
+
146
+ def load_static_registry
147
+ unless File.exist?(@registry_path)
148
+ raise RegistryError, "Model registry file not found at #{@registry_path}"
149
+ end
150
+
151
+ data = YAML.load_file(@registry_path)
152
+ unless data.is_a?(Hash) && data.key?("model_families")
153
+ raise InvalidRegistrySchema, "Registry must contain 'model_families' key"
154
+ end
155
+
156
+ Aidp.log_info("model_registry", "loaded registry", path: @registry_path, families: data["model_families"].size)
157
+ data
158
+ rescue Psych::SyntaxError => e
159
+ raise InvalidRegistrySchema, "Invalid YAML in registry file: #{e.message}"
160
+ end
161
+
162
+ def validate_registry_schema
163
+ @registry_data["model_families"].each do |family, info|
164
+ validate_model_entry(family, info)
165
+ end
166
+ end
167
+
168
+ def validate_model_entry(family, info)
169
+ # Required fields
170
+ unless info["tier"]
171
+ raise InvalidRegistrySchema, "Model family '#{family}' missing required 'tier' field"
172
+ end
173
+ unless VALID_TIERS.include?(info["tier"])
174
+ raise InvalidRegistrySchema, "Model family '#{family}' has invalid tier '#{info["tier"]}'. Valid: #{VALID_TIERS.join(", ")}"
175
+ end
176
+
177
+ # Optional but validated if present
178
+ if info["capabilities"]
179
+ unless info["capabilities"].is_a?(Array)
180
+ raise InvalidRegistrySchema, "Model family '#{family}' capabilities must be an array"
181
+ end
182
+ invalid_caps = info["capabilities"] - VALID_CAPABILITIES
183
+ unless invalid_caps.empty?
184
+ Aidp.log_warn("model_registry", "unknown capabilities", family: family, unknown: invalid_caps)
185
+ end
186
+ end
187
+
188
+ if info["speed"] && !VALID_SPEEDS.include?(info["speed"])
189
+ Aidp.log_warn("model_registry", "invalid speed", family: family, speed: info["speed"], valid: VALID_SPEEDS)
190
+ end
191
+
192
+ # Validate numeric fields if present
193
+ %w[context_window max_output cost_per_1m_input cost_per_1m_output].each do |field|
194
+ if info[field] && !info[field].is_a?(Numeric)
195
+ raise InvalidRegistrySchema, "Model family '#{family}' field '#{field}' must be numeric"
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Harness
5
+ # Filters test and linter output to reduce token consumption
6
+ # Uses framework-specific strategies to extract relevant information
7
+ class OutputFilter
8
+ # Output modes
9
+ MODES = {
10
+ full: :full, # No filtering (default for first run)
11
+ failures_only: :failures_only, # Only failure information
12
+ minimal: :minimal # Minimal failure info + summary
13
+ }.freeze
14
+
15
+ # @param config [Hash] Configuration options
16
+ # @option config [Symbol] :mode Output mode (:full, :failures_only, :minimal)
17
+ # @option config [Boolean] :include_context Include surrounding lines
18
+ # @option config [Integer] :context_lines Number of context lines
19
+ # @option config [Integer] :max_lines Maximum output lines
20
+ def initialize(config = {})
21
+ @mode = config[:mode] || :full
22
+ @include_context = config.fetch(:include_context, true)
23
+ @context_lines = config.fetch(:context_lines, 3)
24
+ @max_lines = config.fetch(:max_lines, 500)
25
+
26
+ validate_mode!
27
+
28
+ Aidp.log_debug("output_filter", "initialized",
29
+ mode: @mode,
30
+ include_context: @include_context,
31
+ max_lines: @max_lines)
32
+ rescue NameError
33
+ # Logging infrastructure not available in some tests
34
+ end
35
+
36
+ # Filter output based on framework and mode
37
+ # @param output [String] Raw output
38
+ # @param framework [Symbol] Framework identifier
39
+ # @return [String] Filtered output
40
+ def filter(output, framework: :unknown)
41
+ return output if @mode == :full
42
+ return "" if output.nil? || output.empty?
43
+
44
+ Aidp.log_debug("output_filter", "filtering_start",
45
+ framework: framework,
46
+ input_lines: output.lines.count)
47
+
48
+ strategy = strategy_for_framework(framework)
49
+ filtered = strategy.filter(output, self)
50
+
51
+ truncated = truncate_if_needed(filtered)
52
+
53
+ Aidp.log_debug("output_filter", "filtering_complete",
54
+ output_lines: truncated.lines.count,
55
+ reduction: reduction_stats(output, truncated))
56
+
57
+ truncated
58
+ rescue NameError
59
+ # Logging infrastructure not available
60
+ return output if @mode == :full
61
+ return "" if output.nil? || output.empty?
62
+
63
+ strategy = strategy_for_framework(framework)
64
+ filtered = strategy.filter(output, self)
65
+ truncate_if_needed(filtered)
66
+ rescue => e
67
+ # External failure - graceful degradation
68
+ begin
69
+ Aidp.log_error("output_filter", "filtering_failed",
70
+ framework: framework,
71
+ mode: @mode,
72
+ error: e.message,
73
+ error_class: e.class.name)
74
+ rescue NameError
75
+ # Logging not available
76
+ end
77
+
78
+ # Return original output as fallback
79
+ output
80
+ end
81
+
82
+ # Accessors for strategy use
83
+ attr_reader :mode, :include_context, :context_lines, :max_lines
84
+
85
+ private
86
+
87
+ def validate_mode!
88
+ unless MODES.key?(@mode)
89
+ raise ArgumentError, "Invalid mode: #{@mode}. Must be one of #{MODES.keys}"
90
+ end
91
+ end
92
+
93
+ def strategy_for_framework(framework)
94
+ case framework
95
+ when :rspec
96
+ require_relative "rspec_filter_strategy"
97
+ RSpecFilterStrategy.new
98
+ when :minitest
99
+ require_relative "generic_filter_strategy"
100
+ GenericFilterStrategy.new
101
+ when :jest
102
+ require_relative "generic_filter_strategy"
103
+ GenericFilterStrategy.new
104
+ when :pytest
105
+ require_relative "generic_filter_strategy"
106
+ GenericFilterStrategy.new
107
+ else
108
+ require_relative "generic_filter_strategy"
109
+ GenericFilterStrategy.new
110
+ end
111
+ end
112
+
113
+ def truncate_if_needed(output)
114
+ lines = output.lines
115
+ return output if lines.count <= @max_lines
116
+
117
+ truncated = lines.first(@max_lines).join
118
+ # Only add newline if truncated doesn't already end with one
119
+ separator = truncated.end_with?("\n") ? "" : "\n"
120
+ truncated + separator + "[Output truncated - #{lines.count - @max_lines} more lines omitted]"
121
+ end
122
+
123
+ def reduction_stats(input, output)
124
+ input_size = input.bytesize
125
+ output_size = output.bytesize
126
+ reduction = ((input_size - output_size).to_f / input_size * 100).round(1)
127
+
128
+ {
129
+ input_bytes: input_size,
130
+ output_bytes: output_size,
131
+ reduction_percent: reduction
132
+ }
133
+ end
134
+ end
135
+ end
136
+ end
@@ -1394,14 +1394,27 @@ module Aidp
1394
1394
 
1395
1395
  # Execute a prompt with a specific provider
1396
1396
  def execute_with_provider(provider_type, prompt, options = {})
1397
+ # Extract model from options if provided
1398
+ model_name = options.delete(:model)
1399
+
1397
1400
  # Create provider factory instance
1398
1401
  provider_factory = ProviderFactory.new
1399
1402
 
1403
+ # Add model to provider options if specified
1404
+ provider_options = options.dup
1405
+ provider_options[:model] = model_name if model_name
1406
+
1400
1407
  # Create provider instance
1401
- provider = provider_factory.create_provider(provider_type, options)
1408
+ provider = provider_factory.create_provider(provider_type, provider_options)
1402
1409
 
1403
- # Set current provider
1410
+ # Set current provider and model
1404
1411
  @current_provider = provider_type
1412
+ @current_model = model_name if model_name
1413
+
1414
+ Aidp.logger.debug("provider_manager", "Executing with provider",
1415
+ provider: provider_type,
1416
+ model: model_name,
1417
+ prompt_length: prompt.length)
1405
1418
 
1406
1419
  # Execute the prompt with the provider
1407
1420
  result = provider.send_message(prompt: prompt, session: nil)
@@ -1410,15 +1423,17 @@ module Aidp
1410
1423
  {
1411
1424
  status: "completed",
1412
1425
  provider: provider_type,
1426
+ model: model_name,
1413
1427
  output: result,
1414
1428
  metadata: {
1415
1429
  provider_type: provider_type,
1430
+ model: model_name,
1416
1431
  prompt_length: prompt.length,
1417
1432
  timestamp: Time.now.strftime("%Y-%m-%dT%H:%M:%S.%3N%z")
1418
1433
  }
1419
1434
  }
1420
1435
  rescue => e
1421
- log_rescue(e, component: "provider_manager", action: "execute_with_provider", fallback: "error_result", provider: provider_type, prompt_length: prompt.length)
1436
+ log_rescue(e, component: "provider_manager", action: "execute_with_provider", fallback: "error_result", provider: provider_type, model: model_name, prompt_length: prompt.length)
1422
1437
  # Return error result
1423
1438
  {
1424
1439
  status: "error",