aidp 0.32.0 → 0.33.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aidp/analyze/feature_analyzer.rb +322 -320
  3. data/lib/aidp/auto_update/coordinator.rb +97 -7
  4. data/lib/aidp/auto_update.rb +0 -12
  5. data/lib/aidp/cli/devcontainer_commands.rb +0 -5
  6. data/lib/aidp/cli.rb +2 -1
  7. data/lib/aidp/comment_consolidator.rb +78 -0
  8. data/lib/aidp/concurrency.rb +0 -3
  9. data/lib/aidp/config.rb +0 -1
  10. data/lib/aidp/config_paths.rb +71 -0
  11. data/lib/aidp/execute/work_loop_runner.rb +324 -15
  12. data/lib/aidp/harness/ai_filter_factory.rb +285 -0
  13. data/lib/aidp/harness/config_schema.rb +97 -1
  14. data/lib/aidp/harness/config_validator.rb +1 -1
  15. data/lib/aidp/harness/configuration.rb +61 -5
  16. data/lib/aidp/harness/filter_definition.rb +212 -0
  17. data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
  18. data/lib/aidp/harness/output_filter.rb +50 -25
  19. data/lib/aidp/harness/output_filter_config.rb +129 -0
  20. data/lib/aidp/harness/provider_manager.rb +90 -2
  21. data/lib/aidp/harness/runner.rb +0 -11
  22. data/lib/aidp/harness/test_runner.rb +179 -41
  23. data/lib/aidp/harness/thinking_depth_manager.rb +16 -0
  24. data/lib/aidp/harness/ui/navigation/submenu.rb +0 -2
  25. data/lib/aidp/loader.rb +195 -0
  26. data/lib/aidp/metadata/compiler.rb +29 -17
  27. data/lib/aidp/metadata/query.rb +1 -1
  28. data/lib/aidp/metadata/scanner.rb +8 -1
  29. data/lib/aidp/metadata/tool_metadata.rb +13 -13
  30. data/lib/aidp/metadata/validator.rb +10 -0
  31. data/lib/aidp/metadata.rb +16 -0
  32. data/lib/aidp/pr_worktree_manager.rb +2 -2
  33. data/lib/aidp/provider_manager.rb +1 -7
  34. data/lib/aidp/setup/wizard.rb +279 -9
  35. data/lib/aidp/skills.rb +0 -5
  36. data/lib/aidp/storage/csv_storage.rb +3 -0
  37. data/lib/aidp/style_guide/selector.rb +360 -0
  38. data/lib/aidp/tooling_detector.rb +283 -16
  39. data/lib/aidp/version.rb +1 -1
  40. data/lib/aidp/watch/change_request_processor.rb +152 -14
  41. data/lib/aidp/watch/repository_client.rb +41 -0
  42. data/lib/aidp/watch/runner.rb +29 -18
  43. data/lib/aidp/watch.rb +5 -7
  44. data/lib/aidp/workstream_cleanup.rb +0 -2
  45. data/lib/aidp/workstream_executor.rb +0 -4
  46. data/lib/aidp/worktree.rb +0 -1
  47. data/lib/aidp.rb +21 -106
  48. metadata +72 -36
  49. data/lib/aidp/config/paths.rb +0 -131
@@ -1240,6 +1240,11 @@ module Aidp
1240
1240
  total_tokens: m[:total_tokens] || 0,
1241
1241
  last_used: m[:last_used]
1242
1242
  }
1243
+
1244
+ # Set reason if currently rate limited (for display in Reason column)
1245
+ if rl && reset_in && reset_in > 0
1246
+ row[:unhealthy_reason] ||= "rate_limited"
1247
+ end
1243
1248
  # Incorporate CLI check outcome into reason/availability if failing
1244
1249
  unless cli_ok_prefetch
1245
1250
  row[:available] = false
@@ -1416,9 +1421,43 @@ module Aidp
1416
1421
 
1417
1422
  # Execute a prompt with a specific provider
1418
1423
  def execute_with_provider(provider_type, prompt, options = {})
1424
+ options = options.dup
1425
+
1419
1426
  # Extract model from options if provided
1420
1427
  model_name = options.delete(:model)
1421
- retry_on_rate_limit = options.delete(:retry_on_rate_limit) != false # Default true
1428
+ retry_on_rate_limit = if options.key?(:retry_on_rate_limit)
1429
+ options.delete(:retry_on_rate_limit) != false
1430
+ else
1431
+ true
1432
+ end
1433
+ retry_on_unsupported = if options.key?(:retry_on_unsupported)
1434
+ options.delete(:retry_on_unsupported) != false
1435
+ else
1436
+ true
1437
+ end
1438
+ tier = options[:tier]
1439
+ base_options = options.dup
1440
+
1441
+ if model_name && model_denied?(provider_type, model_name)
1442
+ alternate_model = select_alternate_model(provider_type, tier: tier, current_model: model_name)
1443
+ if alternate_model
1444
+ Aidp.logger.warn("provider_manager", "Model is denylisted, selecting alternate",
1445
+ provider: provider_type,
1446
+ model: model_name,
1447
+ alternate_model: alternate_model,
1448
+ tier: tier)
1449
+
1450
+ return execute_with_provider(
1451
+ provider_type,
1452
+ prompt,
1453
+ base_options.merge(
1454
+ model: alternate_model,
1455
+ retry_on_rate_limit: retry_on_rate_limit,
1456
+ retry_on_unsupported: false
1457
+ )
1458
+ )
1459
+ end
1460
+ end
1422
1461
 
1423
1462
  # Create provider factory instance
1424
1463
  provider_factory = ProviderFactory.new
@@ -1440,7 +1479,7 @@ module Aidp
1440
1479
  prompt_length: prompt.length)
1441
1480
 
1442
1481
  # Execute the prompt with the provider
1443
- result = provider.send_message(prompt: prompt, session: nil)
1482
+ result = provider.send_message(prompt: prompt, session: nil, options: options)
1444
1483
 
1445
1484
  # Return structured result
1446
1485
  {
@@ -1460,6 +1499,28 @@ module Aidp
1460
1499
 
1461
1500
  if unsupported_model_error?(e, model_name)
1462
1501
  deny_model(provider_type, model_name, error: e)
1502
+
1503
+ if retry_on_unsupported
1504
+ alternate_model = select_alternate_model(provider_type, tier: tier, current_model: model_name)
1505
+
1506
+ if alternate_model
1507
+ Aidp.logger.info("provider_manager", "Retrying with alternate model after unsupported model error",
1508
+ provider: provider_type,
1509
+ original_model: model_name,
1510
+ alternate_model: alternate_model,
1511
+ tier: tier)
1512
+
1513
+ return execute_with_provider(
1514
+ provider_type,
1515
+ prompt,
1516
+ base_options.merge(
1517
+ model: alternate_model,
1518
+ retry_on_rate_limit: retry_on_rate_limit,
1519
+ retry_on_unsupported: false
1520
+ )
1521
+ )
1522
+ end
1523
+ end
1463
1524
  end
1464
1525
 
1465
1526
  # Detect rate limit / quota errors and attempt fallback
@@ -1615,12 +1676,39 @@ module Aidp
1615
1676
 
1616
1677
  message = error&.message.to_s.downcase
1617
1678
  return false if message.empty?
1679
+ return false unless message.include?(model_name.to_s.downcase)
1618
1680
 
1619
1681
  (message.include?("unsupported") && message.include?("model")) ||
1620
1682
  (message.include?("model") && message.include?("not supported")) ||
1621
1683
  message.include?("invalid model")
1622
1684
  end
1623
1685
 
1686
+ # Select an alternate model for the provider and tier, excluding denylisted/current models
1687
+ def select_alternate_model(provider_name, tier:, current_model: nil)
1688
+ candidates = []
1689
+
1690
+ if tier && @configuration.respond_to?(:models_for_tier)
1691
+ tier_models = Array(@configuration.models_for_tier(tier, provider_name)).map(&:to_s)
1692
+ candidates.concat(tier_models)
1693
+ end
1694
+
1695
+ if candidates.empty? && @configuration.respond_to?(:provider_models)
1696
+ provider_models = Array(@configuration.provider_models(provider_name)).map(&:to_s)
1697
+ candidates.concat(provider_models)
1698
+ end
1699
+
1700
+ candidates = candidates.uniq
1701
+ return nil if candidates.empty?
1702
+
1703
+ excluded = Array(current_model).compact + Array(@model_denylist[provider_name])
1704
+ candidates.reject! { |model| excluded.include?(model) }
1705
+ candidates.first
1706
+ rescue => e
1707
+ log_rescue(e, component: "provider_manager", action: "select_alternate_model", fallback: nil,
1708
+ provider: provider_name, tier: tier, current_model: current_model)
1709
+ nil
1710
+ end
1711
+
1624
1712
  public
1625
1713
 
1626
1714
  # Log provider switch
@@ -2,16 +2,6 @@
2
2
 
3
3
  require "timeout"
4
4
  require "json"
5
- require_relative "configuration"
6
- require_relative "state_manager"
7
- require_relative "condition_detector"
8
- require_relative "provider_manager"
9
- require_relative "simple_user_interface"
10
- require_relative "error_handler"
11
- require_relative "status_display"
12
- require_relative "completion_checker"
13
- require_relative "../concurrency"
14
- require_relative "../errors"
15
5
 
16
6
  module Aidp
17
7
  module Harness
@@ -61,7 +51,6 @@ module Aidp
61
51
  # Use ZFC-enabled condition detector
62
52
  # ZfcConditionDetector will create its own ProviderFactory if needed
63
53
  # Falls back to legacy pattern matching when ZFC is disabled
64
- require_relative "zfc_condition_detector"
65
54
  @condition_detector = ZfcConditionDetector.new(@configuration)
66
55
 
67
56
  @user_interface = SimpleUserInterface.new
@@ -3,16 +3,19 @@
3
3
  require "open3"
4
4
  require_relative "../tooling_detector"
5
5
  require_relative "output_filter"
6
+ require_relative "output_filter_config"
6
7
 
7
8
  module Aidp
8
9
  module Harness
9
10
  # Executes test and linter commands configured in aidp.yml
10
11
  # Returns results with exit status and output
12
+ # Applies intelligent output filtering to reduce token consumption
11
13
  class TestRunner
12
14
  def initialize(project_dir, config)
13
15
  @project_dir = project_dir
14
16
  @config = config
15
17
  @iteration_count = 0
18
+ @filter_stats = {total_input_bytes: 0, total_output_bytes: 0}
16
19
  end
17
20
 
18
21
  # Run all configured tests
@@ -47,6 +50,29 @@ module Aidp
47
50
  run_command_category(:documentation, "Documentation")
48
51
  end
49
52
 
53
+ # Preview the commands that will run for each category so callers can log intent
54
+ # Returns a hash of category => array of normalized command entries
55
+ def planned_commands
56
+ {
57
+ tests: resolved_commands(:test),
58
+ lints: resolved_commands(:lint),
59
+ formatters: resolved_commands(:formatter),
60
+ builds: resolved_commands(:build),
61
+ docs: resolved_commands(:documentation)
62
+ }
63
+ end
64
+
65
+ # Get current iteration count
66
+ attr_reader :iteration_count
67
+
68
+ # Get filtering statistics
69
+ attr_reader :filter_stats
70
+
71
+ # Reset iteration counter (useful for testing)
72
+ def reset_iteration_count
73
+ @iteration_count = 0
74
+ end
75
+
50
76
  private
51
77
 
52
78
  # Run commands for a specific category (test, lint, formatter, build, documentation)
@@ -59,6 +85,12 @@ module Aidp
59
85
  # Determine output mode based on category
60
86
  mode = determine_output_mode(category)
61
87
 
88
+ Aidp.log_debug("test_runner", "running_category",
89
+ category: category,
90
+ command_count: commands.length,
91
+ iteration: @iteration_count,
92
+ output_mode: mode)
93
+
62
94
  # Execute all commands
63
95
  results = commands.map do |cmd_config|
64
96
  # Handle both string commands (legacy) and hash format (new)
@@ -71,6 +103,23 @@ module Aidp
71
103
  end
72
104
  end
73
105
 
106
+ aggregate_results(results, display_name, mode: mode)
107
+ rescue NameError
108
+ # Logging not available
109
+ commands = resolved_commands(category)
110
+ return {success: true, output: "", failures: [], required_failures: []} if commands.empty?
111
+
112
+ mode = determine_output_mode(category)
113
+ results = commands.map do |cmd_config|
114
+ if cmd_config.is_a?(String)
115
+ result = execute_command(cmd_config, category.to_s)
116
+ result.merge(required: true)
117
+ else
118
+ result = execute_command(cmd_config[:command], category.to_s)
119
+ result.merge(required: cmd_config[:required])
120
+ end
121
+ end
122
+
74
123
  aggregate_results(results, display_name, mode: mode)
75
124
  end
76
125
 
@@ -107,6 +156,11 @@ module Aidp
107
156
  (optional_failures.any? ? "\n" + format_failures(optional_failures, "#{category} - Optional", mode: mode) : "")
108
157
  end
109
158
 
159
+ # Add filtering summary if filtering was applied
160
+ if mode != :full && all_failures.any?
161
+ output += "\n[Output filtered: mode=#{mode}]"
162
+ end
163
+
110
164
  {
111
165
  success: success,
112
166
  output: output,
@@ -124,9 +178,12 @@ module Aidp
124
178
  output << "Exit Code: #{failure[:exit_code]}"
125
179
  output << "--- Output ---"
126
180
 
181
+ # Detect framework for filtering
182
+ framework = detect_framework_from_command(failure[:command])
183
+
127
184
  # 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)
185
+ filtered_stdout = filter_output(failure[:stdout], mode, framework, :test)
186
+ filtered_stderr = filter_output(failure[:stderr], mode, :unknown, :lint)
130
187
 
131
188
  output << filtered_stdout unless filtered_stdout.strip.empty?
132
189
  output << filtered_stderr unless filtered_stderr.strip.empty?
@@ -136,67 +193,139 @@ module Aidp
136
193
  output.join("\n")
137
194
  end
138
195
 
139
- def filter_output(raw_output, mode, framework)
196
+ def filter_output(raw_output, mode, framework, category)
140
197
  return raw_output if mode == :full || raw_output.nil? || raw_output.empty?
141
198
 
142
- filter_config = {
199
+ # Build filter config from configuration or defaults
200
+ filter_config = build_filter_config(mode, category)
201
+
202
+ # Load AI-generated filter definition if available (AGD pattern)
203
+ filter_definition = load_filter_definition_for(category)
204
+
205
+ # Track input size for stats
206
+ @filter_stats[:total_input_bytes] += raw_output.bytesize
207
+
208
+ filter = OutputFilter.new(filter_config, filter_definition: filter_definition)
209
+ filtered = filter.filter(raw_output, framework: framework)
210
+
211
+ # Track output size for stats
212
+ @filter_stats[:total_output_bytes] += filtered.bytesize
213
+
214
+ Aidp.log_debug("test_runner", "output_filtered",
215
+ framework: framework,
143
216
  mode: mode,
144
- include_context: true,
145
- context_lines: 3,
146
- max_lines: 500
147
- }
217
+ input_size: raw_output.bytesize,
218
+ output_size: filtered.bytesize,
219
+ has_custom_definition: !filter_definition.nil?)
148
220
 
149
- filter = OutputFilter.new(filter_config)
150
- filter.filter(raw_output, framework: framework)
221
+ filtered
151
222
  rescue NameError
152
223
  # Logging infrastructure not available
153
224
  raw_output
154
225
  rescue => e
155
- Aidp.log_warn("test_runner", "filter_failed",
156
- error: e.message,
157
- framework: framework)
226
+ begin
227
+ Aidp.log_warn("test_runner", "filter_failed",
228
+ error: e.message,
229
+ framework: framework)
230
+ rescue NameError
231
+ # Logging not available
232
+ end
158
233
  raw_output # Fallback to unfiltered on error
159
234
  end
160
235
 
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
236
+ # Load AI-generated filter definition for a category
237
+ # @param category [Symbol] :test or :lint
238
+ # @return [FilterDefinition, nil]
239
+ def load_filter_definition_for(category)
240
+ return nil unless @config.respond_to?(:filter_definition_for)
241
+
242
+ # Map category to definition key (matches setup wizard keys)
243
+ definition_key = case category
244
+ when :test then "unit_test"
245
+ when :lint then "lint"
246
+ else category.to_s
173
247
  end
248
+
249
+ @config.filter_definition_for(definition_key)
250
+ end
251
+
252
+ def build_filter_config(mode, category)
253
+ config_hash = {
254
+ mode: mode,
255
+ include_context: output_filtering_include_context,
256
+ context_lines: output_filtering_context_lines,
257
+ max_lines: max_output_lines_for_category(category)
258
+ }
259
+
260
+ OutputFilterConfig.from_hash(config_hash)
261
+ end
262
+
263
+ def detect_framework_from_command(command)
264
+ # Use ToolingDetector's enhanced framework detection
265
+ Aidp::ToolingDetector.framework_from_command(command)
174
266
  end
175
267
 
176
268
  def determine_output_mode(category)
177
269
  # Check config for category-specific mode
178
270
  case category
179
271
  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
272
+ configured_mode = test_output_mode_from_config
273
+ return configured_mode if configured_mode
274
+
275
+ # Default: full on first iteration, failures_only after
276
+ (@iteration_count > 1) ? :failures_only : :full
187
277
  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
278
+ configured_mode = lint_output_mode_from_config
279
+ return configured_mode if configured_mode
280
+
281
+ # Default: full on first iteration, failures_only after
282
+ (@iteration_count > 1) ? :failures_only : :full
195
283
  else
196
284
  :full
197
285
  end
198
286
  end
199
287
 
288
+ def test_output_mode_from_config
289
+ return nil unless @config.respond_to?(:test_output_mode)
290
+ mode = @config.test_output_mode
291
+ return nil if mode.nil?
292
+ mode.to_sym
293
+ rescue
294
+ nil
295
+ end
296
+
297
+ def lint_output_mode_from_config
298
+ return nil unless @config.respond_to?(:lint_output_mode)
299
+ mode = @config.lint_output_mode
300
+ return nil if mode.nil?
301
+ mode.to_sym
302
+ rescue
303
+ nil
304
+ end
305
+
306
+ def max_output_lines_for_category(category)
307
+ case category
308
+ when :test
309
+ return @config.test_max_output_lines if @config.respond_to?(:test_max_output_lines) && @config.test_max_output_lines
310
+ OutputFilterConfig::DEFAULT_MAX_LINES
311
+ when :lint
312
+ return @config.lint_max_output_lines if @config.respond_to?(:lint_max_output_lines) && @config.lint_max_output_lines
313
+ 300 # Default smaller for lint output
314
+ else
315
+ OutputFilterConfig::DEFAULT_MAX_LINES
316
+ end
317
+ end
318
+
319
+ def output_filtering_include_context
320
+ return @config.output_filtering_include_context if @config.respond_to?(:output_filtering_include_context)
321
+ OutputFilterConfig::DEFAULT_INCLUDE_CONTEXT
322
+ end
323
+
324
+ def output_filtering_context_lines
325
+ return @config.output_filtering_context_lines if @config.respond_to?(:output_filtering_context_lines)
326
+ OutputFilterConfig::DEFAULT_CONTEXT_LINES
327
+ end
328
+
200
329
  # Resolve commands for a specific category
201
330
  # Returns normalized command configs (array of {command:, required:} hashes)
202
331
  def resolved_commands(category)
@@ -239,8 +368,17 @@ module Aidp
239
368
  def detected_tooling
240
369
  @detected_tooling ||= Aidp::ToolingDetector.detect(@project_dir)
241
370
  rescue => e
242
- Aidp.log_warn("test_runner", "tooling_detection_failed", error: e.message)
243
- Aidp::ToolingDetector::Result.new(test_commands: [], lint_commands: [])
371
+ begin
372
+ Aidp.log_warn("test_runner", "tooling_detection_failed", error: e.message)
373
+ rescue NameError
374
+ # Logging not available
375
+ end
376
+ Aidp::ToolingDetector::Result.new(
377
+ test_commands: [],
378
+ lint_commands: [],
379
+ formatter_commands: [],
380
+ frameworks: {}
381
+ )
244
382
  end
245
383
 
246
384
  def log_fallback(type, commands)
@@ -152,6 +152,15 @@ module Aidp
152
152
  def select_model_for_tier(tier = nil, provider: nil)
153
153
  tier ||= current_tier
154
154
  validate_tier!(tier)
155
+ provider_has_no_tiers = provider && configuration.configured_tiers(provider).empty?
156
+ provider_has_catalog_models = provider && !@registry.models_for_provider(provider).empty?
157
+
158
+ if provider_has_no_tiers && !provider_has_catalog_models
159
+ Aidp.log_info("thinking_depth_manager", "No configured tiers for provider, deferring to provider auto model selection",
160
+ requested_tier: tier,
161
+ provider: provider)
162
+ return [provider, nil, {auto_model: true, reason: "provider_has_no_tiers"}]
163
+ end
155
164
 
156
165
  # First, try to get models from user's configuration for this tier and provider
157
166
  if provider
@@ -270,6 +279,13 @@ module Aidp
270
279
 
271
280
  result = try_fallback_tiers(tier, provider)
272
281
 
282
+ if provider_has_no_tiers && result.nil? && provider
283
+ Aidp.log_info("thinking_depth_manager", "No configured tiers for provider, deferring to provider auto model selection",
284
+ requested_tier: tier,
285
+ provider: provider)
286
+ return [provider, nil, {auto_model: true, reason: "provider_has_no_tiers"}]
287
+ end
288
+
273
289
  unless result
274
290
  # Enhanced error message with discovery hints
275
291
  display_enhanced_tier_error(tier, provider)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "main_menu"
4
-
5
3
  module Aidp
6
4
  module Harness
7
5
  module UI