aidp 0.32.0 → 0.34.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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/lib/aidp/analyze/feature_analyzer.rb +322 -320
  4. data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
  5. data/lib/aidp/auto_update/coordinator.rb +97 -7
  6. data/lib/aidp/auto_update.rb +0 -12
  7. data/lib/aidp/cli/devcontainer_commands.rb +0 -5
  8. data/lib/aidp/cli/eval_command.rb +399 -0
  9. data/lib/aidp/cli/harness_command.rb +1 -1
  10. data/lib/aidp/cli/security_command.rb +416 -0
  11. data/lib/aidp/cli/tools_command.rb +6 -4
  12. data/lib/aidp/cli.rb +172 -4
  13. data/lib/aidp/comment_consolidator.rb +78 -0
  14. data/lib/aidp/concurrency/exec.rb +3 -0
  15. data/lib/aidp/concurrency.rb +0 -3
  16. data/lib/aidp/config.rb +113 -1
  17. data/lib/aidp/config_paths.rb +91 -0
  18. data/lib/aidp/daemon/runner.rb +8 -4
  19. data/lib/aidp/errors.rb +134 -0
  20. data/lib/aidp/evaluations/context_capture.rb +205 -0
  21. data/lib/aidp/evaluations/evaluation_record.rb +114 -0
  22. data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
  23. data/lib/aidp/evaluations.rb +23 -0
  24. data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
  25. data/lib/aidp/execute/interactive_repl.rb +6 -2
  26. data/lib/aidp/execute/prompt_evaluator.rb +359 -0
  27. data/lib/aidp/execute/repl_macros.rb +100 -1
  28. data/lib/aidp/execute/work_loop_runner.rb +719 -58
  29. data/lib/aidp/execute/work_loop_state.rb +4 -1
  30. data/lib/aidp/execute/workflow_selector.rb +3 -0
  31. data/lib/aidp/harness/ai_decision_engine.rb +79 -0
  32. data/lib/aidp/harness/ai_filter_factory.rb +285 -0
  33. data/lib/aidp/harness/capability_registry.rb +2 -0
  34. data/lib/aidp/harness/condition_detector.rb +3 -0
  35. data/lib/aidp/harness/config_loader.rb +3 -0
  36. data/lib/aidp/harness/config_schema.rb +97 -1
  37. data/lib/aidp/harness/config_validator.rb +1 -1
  38. data/lib/aidp/harness/configuration.rb +61 -5
  39. data/lib/aidp/harness/enhanced_runner.rb +14 -11
  40. data/lib/aidp/harness/error_handler.rb +3 -0
  41. data/lib/aidp/harness/filter_definition.rb +212 -0
  42. data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
  43. data/lib/aidp/harness/output_filter.rb +50 -25
  44. data/lib/aidp/harness/output_filter_config.rb +129 -0
  45. data/lib/aidp/harness/provider_factory.rb +3 -0
  46. data/lib/aidp/harness/provider_manager.rb +96 -2
  47. data/lib/aidp/harness/runner.rb +5 -12
  48. data/lib/aidp/harness/state/persistence.rb +3 -0
  49. data/lib/aidp/harness/state_manager.rb +3 -0
  50. data/lib/aidp/harness/status_display.rb +28 -20
  51. data/lib/aidp/harness/test_runner.rb +179 -41
  52. data/lib/aidp/harness/thinking_depth_manager.rb +44 -28
  53. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
  54. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
  55. data/lib/aidp/harness/ui/error_handler.rb +3 -0
  56. data/lib/aidp/harness/ui/job_monitor.rb +4 -0
  57. data/lib/aidp/harness/ui/navigation/submenu.rb +2 -2
  58. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
  59. data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
  60. data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
  61. data/lib/aidp/harness/user_interface.rb +3 -0
  62. data/lib/aidp/loader.rb +195 -0
  63. data/lib/aidp/logger.rb +3 -0
  64. data/lib/aidp/message_display.rb +31 -0
  65. data/lib/aidp/metadata/compiler.rb +29 -17
  66. data/lib/aidp/metadata/query.rb +1 -1
  67. data/lib/aidp/metadata/scanner.rb +8 -1
  68. data/lib/aidp/metadata/tool_metadata.rb +13 -13
  69. data/lib/aidp/metadata/validator.rb +10 -0
  70. data/lib/aidp/metadata.rb +16 -0
  71. data/lib/aidp/pr_worktree_manager.rb +20 -8
  72. data/lib/aidp/provider_manager.rb +4 -7
  73. data/lib/aidp/providers/base.rb +2 -0
  74. data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
  75. data/lib/aidp/security/secrets_proxy.rb +328 -0
  76. data/lib/aidp/security/secrets_registry.rb +227 -0
  77. data/lib/aidp/security/trifecta_state.rb +220 -0
  78. data/lib/aidp/security/watch_mode_handler.rb +306 -0
  79. data/lib/aidp/security/work_loop_adapter.rb +277 -0
  80. data/lib/aidp/security.rb +56 -0
  81. data/lib/aidp/setup/wizard.rb +283 -11
  82. data/lib/aidp/skills.rb +0 -5
  83. data/lib/aidp/storage/csv_storage.rb +3 -0
  84. data/lib/aidp/style_guide/selector.rb +360 -0
  85. data/lib/aidp/tooling_detector.rb +283 -16
  86. data/lib/aidp/version.rb +1 -1
  87. data/lib/aidp/watch/auto_merger.rb +274 -0
  88. data/lib/aidp/watch/auto_pr_processor.rb +125 -7
  89. data/lib/aidp/watch/build_processor.rb +16 -1
  90. data/lib/aidp/watch/change_request_processor.rb +682 -150
  91. data/lib/aidp/watch/ci_fix_processor.rb +262 -4
  92. data/lib/aidp/watch/feedback_collector.rb +191 -0
  93. data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
  94. data/lib/aidp/watch/implementation_verifier.rb +142 -1
  95. data/lib/aidp/watch/plan_generator.rb +70 -13
  96. data/lib/aidp/watch/plan_processor.rb +12 -5
  97. data/lib/aidp/watch/projects_processor.rb +286 -0
  98. data/lib/aidp/watch/repository_client.rb +871 -22
  99. data/lib/aidp/watch/review_processor.rb +33 -6
  100. data/lib/aidp/watch/runner.rb +80 -29
  101. data/lib/aidp/watch/state_store.rb +233 -0
  102. data/lib/aidp/watch/sub_issue_creator.rb +221 -0
  103. data/lib/aidp/watch.rb +5 -7
  104. data/lib/aidp/workflows/guided_agent.rb +4 -0
  105. data/lib/aidp/workstream_cleanup.rb +0 -2
  106. data/lib/aidp/workstream_executor.rb +3 -4
  107. data/lib/aidp/worktree.rb +61 -12
  108. data/lib/aidp/worktree_branch_manager.rb +347 -101
  109. data/lib/aidp.rb +21 -106
  110. data/templates/implementation/iterative_implementation.md +46 -3
  111. metadata +91 -36
  112. data/lib/aidp/config/paths.rb +0 -131
@@ -23,6 +23,15 @@ module Aidp
23
23
  error: "error"
24
24
  }.freeze
25
25
 
26
+ # Expose state for testability
27
+ attr_accessor :state, :current_step, :start_time
28
+ attr_writer :current_provider, :user_input, :execution_log
29
+ attr_accessor :mode, :project_dir, :selected_steps, :workflow_type
30
+ attr_reader :provider_manager, :sleeper
31
+ attr_writer :completion_checker, :workflow_controller, :error_handler
32
+ attr_accessor :condition_detector
33
+ attr_writer :state_manager, :configuration
34
+
26
35
  # Simple sleeper abstraction for test control
27
36
  class Sleeper
28
37
  def sleep(duration)
@@ -75,27 +84,21 @@ module Aidp
75
84
  @completion_checker = options[:completion_checker] || CompletionChecker.new(@project_dir, @workflow_type)
76
85
  end
77
86
 
78
- # Get current provider (delegate to provider manager)
87
+ # Get current provider (delegate to provider manager with fallback)
79
88
  def current_provider
80
89
  @current_provider || @provider_manager&.current_provider || "unknown"
81
90
  end
82
91
 
83
- # Get current step
84
- attr_reader :current_step
85
-
86
- # Get user input
92
+ # Get user input (with nil safety)
87
93
  def user_input
88
94
  @user_input || {}
89
95
  end
90
96
 
91
- # Get execution log
97
+ # Get execution log (with nil safety)
92
98
  def execution_log
93
99
  @execution_log || []
94
100
  end
95
101
 
96
- # Get provider manager
97
- attr_reader :provider_manager
98
-
99
102
  # Main execution method with enhanced TUI
100
103
  def run
101
104
  @state = STATES[:running]
@@ -354,7 +357,7 @@ module Aidp
354
357
  duration: @start_time ? Time.now - @start_time : 0,
355
358
  user_input_count: @user_input.size,
356
359
  execution_log_count: @execution_log.size,
357
- jobs_count: @tui.instance_variable_get(:@jobs).size
360
+ jobs_count: @tui.jobs.size
358
361
  }
359
362
  end
360
363
 
@@ -530,7 +533,7 @@ module Aidp
530
533
 
531
534
  def cleanup
532
535
  # Cleanup any remaining jobs
533
- @tui.instance_variable_get(:@jobs).keys.each do |job_id|
536
+ @tui.jobs.keys.each do |job_id|
534
537
  @tui.remove_job(job_id)
535
538
  end
536
539
  end
@@ -12,6 +12,9 @@ module Aidp
12
12
  class ErrorHandler
13
13
  include Aidp::DebugMixin
14
14
 
15
+ # Expose internal components for testability
16
+ attr_reader :retry_strategies, :backoff_calculator, :error_classifier, :recovery_planner
17
+
15
18
  # Simple wrapper to allow dependency injection of sleep behavior in tests
16
19
  class Sleeper
17
20
  def sleep(seconds)
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Harness
5
+ # Value object representing a generated filter definition
6
+ # Created by AI during configuration, applied deterministically at runtime
7
+ #
8
+ # @example Usage
9
+ # definition = FilterDefinition.new(
10
+ # tool_name: "pytest",
11
+ # summary_patterns: ["\\d+ passed", "\\d+ failed"],
12
+ # failure_section_start: "=+ FAILURES =+",
13
+ # failure_section_end: "=+ short test summary",
14
+ # error_patterns: ["AssertionError", "Error:"],
15
+ # location_patterns: ["\\S+\\.py:\\d+"],
16
+ # noise_patterns: ["^\\s*$", "^platform "]
17
+ # )
18
+ #
19
+ # @see AIFilterFactory for generation
20
+ # @see GeneratedFilterStrategy for runtime application
21
+ class FilterDefinition
22
+ attr_reader :tool_name, :tool_command, :summary_patterns, :failure_section_start,
23
+ :failure_section_end, :error_section_start, :error_section_end,
24
+ :error_patterns, :location_patterns, :noise_patterns,
25
+ :important_patterns, :context_lines, :created_at
26
+
27
+ # Initialize a filter definition
28
+ #
29
+ # @param tool_name [String] Human-readable tool name (e.g., "pytest", "eslint")
30
+ # @param tool_command [String, nil] The command used to run the tool
31
+ # @param summary_patterns [Array<String>] Regex patterns that match summary lines
32
+ # @param failure_section_start [String, nil] Regex pattern marking start of failures section
33
+ # @param failure_section_end [String, nil] Regex pattern marking end of failures section
34
+ # @param error_section_start [String, nil] Regex pattern marking start of errors section
35
+ # @param error_section_end [String, nil] Regex pattern marking end of errors section
36
+ # @param error_patterns [Array<String>] Regex patterns that match error indicators
37
+ # @param location_patterns [Array<String>] Regex patterns that extract file:line locations
38
+ # @param noise_patterns [Array<String>] Regex patterns for lines to filter out
39
+ # @param important_patterns [Array<String>] Regex patterns for lines to always keep
40
+ # @param context_lines [Integer] Number of context lines around failures
41
+ # @param created_at [Time, nil] When this definition was generated
42
+ def initialize(
43
+ tool_name:,
44
+ tool_command: nil,
45
+ summary_patterns: [],
46
+ failure_section_start: nil,
47
+ failure_section_end: nil,
48
+ error_section_start: nil,
49
+ error_section_end: nil,
50
+ error_patterns: [],
51
+ location_patterns: [],
52
+ noise_patterns: [],
53
+ important_patterns: [],
54
+ context_lines: 3,
55
+ created_at: nil
56
+ )
57
+ @tool_name = tool_name
58
+ @tool_command = tool_command
59
+ @summary_patterns = compile_patterns(summary_patterns)
60
+ @failure_section_start = compile_pattern(failure_section_start)
61
+ @failure_section_end = compile_pattern(failure_section_end)
62
+ @error_section_start = compile_pattern(error_section_start)
63
+ @error_section_end = compile_pattern(error_section_end)
64
+ @error_patterns = compile_patterns(error_patterns)
65
+ @location_patterns = compile_patterns(location_patterns)
66
+ @noise_patterns = compile_patterns(noise_patterns)
67
+ @important_patterns = compile_patterns(important_patterns)
68
+ @context_lines = context_lines
69
+ @created_at = created_at || Time.now
70
+
71
+ freeze
72
+ end
73
+
74
+ # Create from a hash (e.g., loaded from YAML config)
75
+ #
76
+ # @param hash [Hash] Definition data with string or symbol keys
77
+ # @return [FilterDefinition]
78
+ def self.from_hash(hash)
79
+ hash = hash.transform_keys(&:to_sym)
80
+
81
+ new(
82
+ tool_name: hash[:tool_name] || "unknown",
83
+ tool_command: hash[:tool_command],
84
+ summary_patterns: Array(hash[:summary_patterns]),
85
+ failure_section_start: hash[:failure_section_start],
86
+ failure_section_end: hash[:failure_section_end],
87
+ error_section_start: hash[:error_section_start],
88
+ error_section_end: hash[:error_section_end],
89
+ error_patterns: Array(hash[:error_patterns]),
90
+ location_patterns: Array(hash[:location_patterns]),
91
+ noise_patterns: Array(hash[:noise_patterns]),
92
+ important_patterns: Array(hash[:important_patterns]),
93
+ context_lines: hash[:context_lines] || 3,
94
+ created_at: hash[:created_at] ? Time.parse(hash[:created_at].to_s) : nil
95
+ )
96
+ end
97
+
98
+ # Convert to hash for serialization (e.g., saving to YAML)
99
+ #
100
+ # @return [Hash] Serializable representation
101
+ def to_h
102
+ {
103
+ tool_name: @tool_name,
104
+ tool_command: @tool_command,
105
+ summary_patterns: patterns_to_strings(@summary_patterns),
106
+ failure_section_start: pattern_to_string(@failure_section_start),
107
+ failure_section_end: pattern_to_string(@failure_section_end),
108
+ error_section_start: pattern_to_string(@error_section_start),
109
+ error_section_end: pattern_to_string(@error_section_end),
110
+ error_patterns: patterns_to_strings(@error_patterns),
111
+ location_patterns: patterns_to_strings(@location_patterns),
112
+ noise_patterns: patterns_to_strings(@noise_patterns),
113
+ important_patterns: patterns_to_strings(@important_patterns),
114
+ context_lines: @context_lines,
115
+ created_at: @created_at&.iso8601
116
+ }.compact
117
+ end
118
+
119
+ # Check if this definition has failure section markers
120
+ #
121
+ # @return [Boolean]
122
+ def has_failure_section?
123
+ !@failure_section_start.nil?
124
+ end
125
+
126
+ # Check if this definition has error section markers
127
+ #
128
+ # @return [Boolean]
129
+ def has_error_section?
130
+ !@error_section_start.nil?
131
+ end
132
+
133
+ # Check if a line matches any summary pattern
134
+ #
135
+ # @param line [String] Line to check
136
+ # @return [Boolean]
137
+ def summary_line?(line)
138
+ @summary_patterns.any? { |pattern| line.match?(pattern) }
139
+ end
140
+
141
+ # Check if a line matches any error pattern
142
+ #
143
+ # @param line [String] Line to check
144
+ # @return [Boolean]
145
+ def error_line?(line)
146
+ @error_patterns.any? { |pattern| line.match?(pattern) }
147
+ end
148
+
149
+ # Check if a line should be filtered as noise
150
+ #
151
+ # @param line [String] Line to check
152
+ # @return [Boolean]
153
+ def noise_line?(line)
154
+ @noise_patterns.any? { |pattern| line.match?(pattern) }
155
+ end
156
+
157
+ # Check if a line should always be kept
158
+ #
159
+ # @param line [String] Line to check
160
+ # @return [Boolean]
161
+ def important_line?(line)
162
+ @important_patterns.any? { |pattern| line.match?(pattern) }
163
+ end
164
+
165
+ # Extract file locations from a line
166
+ #
167
+ # @param line [String] Line to extract locations from
168
+ # @return [Array<String>] Extracted locations
169
+ def extract_locations(line)
170
+ @location_patterns.flat_map do |pattern|
171
+ line.scan(pattern).flatten
172
+ end.compact.uniq
173
+ end
174
+
175
+ # Equality based on all attributes
176
+ def ==(other)
177
+ return false unless other.is_a?(FilterDefinition)
178
+
179
+ to_h == other.to_h
180
+ end
181
+
182
+ alias_method :eql?, :==
183
+
184
+ def hash
185
+ to_h.hash
186
+ end
187
+
188
+ private
189
+
190
+ def compile_patterns(strings)
191
+ Array(strings).map { |s| compile_pattern(s) }.compact
192
+ end
193
+
194
+ def compile_pattern(string)
195
+ return nil if string.nil? || string.empty?
196
+ Regexp.new(string, Regexp::IGNORECASE)
197
+ rescue RegexpError => e
198
+ Aidp.log_warn("filter_definition", "Invalid regex pattern",
199
+ pattern: string, error: e.message)
200
+ nil
201
+ end
202
+
203
+ def patterns_to_strings(patterns)
204
+ patterns.map { |p| p&.source }.compact
205
+ end
206
+
207
+ def pattern_to_string(pattern)
208
+ pattern&.source
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "filter_strategy"
4
+ require_relative "filter_definition"
5
+
6
+ module Aidp
7
+ module Harness
8
+ # Deterministic filter strategy using AI-generated patterns
9
+ #
10
+ # This strategy applies a FilterDefinition at runtime without any AI calls.
11
+ # The patterns were generated by AIFilterFactory during configuration.
12
+ #
13
+ # @example Usage
14
+ # definition = FilterDefinition.from_hash(config[:filter_definitions][:pytest])
15
+ # strategy = GeneratedFilterStrategy.new(definition)
16
+ # filtered = strategy.filter(raw_output, filter_instance)
17
+ #
18
+ # @see FilterDefinition for the pattern specification
19
+ # @see AIFilterFactory for how definitions are generated
20
+ class GeneratedFilterStrategy < FilterStrategy
21
+ attr_reader :definition
22
+
23
+ # Initialize with a filter definition
24
+ #
25
+ # @param definition [FilterDefinition] The AI-generated filter definition
26
+ def initialize(definition)
27
+ @definition = definition
28
+ end
29
+
30
+ # Filter output using the definition's patterns
31
+ #
32
+ # @param output [String] Raw output to filter
33
+ # @param filter_instance [OutputFilter] Filter instance with mode and config
34
+ # @return [String] Filtered output
35
+ def filter(output, filter_instance)
36
+ return output if output.nil? || output.empty?
37
+
38
+ case filter_instance.mode
39
+ when :failures_only
40
+ extract_failures_only(output)
41
+ when :minimal
42
+ extract_minimal(output)
43
+ else
44
+ output
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # Extract failures and summary for failures_only mode
51
+ def extract_failures_only(output)
52
+ lines = output.lines
53
+ parts = []
54
+
55
+ # Extract summary lines
56
+ summary_lines = extract_summary_lines(lines)
57
+ if summary_lines.any?
58
+ parts << "#{@definition.tool_name} Summary:"
59
+ parts.concat(summary_lines.map(&:strip))
60
+ parts << ""
61
+ end
62
+
63
+ # Extract failure section if markers defined
64
+ if @definition.has_failure_section?
65
+ failure_content = extract_section(lines,
66
+ @definition.failure_section_start,
67
+ @definition.failure_section_end)
68
+ if failure_content.any?
69
+ parts << "Failures:"
70
+ parts.concat(failure_content)
71
+ parts << ""
72
+ end
73
+ end
74
+
75
+ # Extract error section if different from failures
76
+ if @definition.has_error_section?
77
+ error_content = extract_section(lines,
78
+ @definition.error_section_start,
79
+ @definition.error_section_end)
80
+ if error_content.any?
81
+ parts << "Errors:"
82
+ parts.concat(error_content)
83
+ parts << ""
84
+ end
85
+ end
86
+
87
+ # If no section markers are defined, extract lines with error patterns
88
+ # This handles tools that don't have structured failure sections
89
+ unless @definition.has_failure_section? || @definition.has_error_section?
90
+ error_lines = extract_error_lines(lines)
91
+ if error_lines.any?
92
+ parts << "Issues:"
93
+ parts.concat(error_lines)
94
+ parts << ""
95
+ end
96
+ end
97
+
98
+ result = parts.join("\n")
99
+ result.empty? ? output : result
100
+ end
101
+
102
+ # Extract minimal information (summary + locations only)
103
+ def extract_minimal(output)
104
+ lines = output.lines
105
+ parts = []
106
+
107
+ # Extract summary
108
+ summary_lines = extract_summary_lines(lines)
109
+ parts.concat(summary_lines.map(&:strip))
110
+
111
+ # Extract all file locations
112
+ locations = extract_all_locations(lines)
113
+ if locations.any?
114
+ parts << ""
115
+ parts << "Locations:"
116
+ parts.concat(locations.uniq.map { |loc| " #{loc}" })
117
+ end
118
+
119
+ parts.join("\n")
120
+ end
121
+
122
+ # Extract lines matching summary patterns
123
+ def extract_summary_lines(lines)
124
+ lines.select { |line| @definition.summary_line?(line) }
125
+ end
126
+
127
+ # Extract content between section markers
128
+ def extract_section(lines, start_pattern, end_pattern)
129
+ return [] unless start_pattern
130
+
131
+ content = []
132
+ in_section = false
133
+
134
+ lines.each do |line|
135
+ if !in_section && line.match?(start_pattern)
136
+ in_section = true
137
+ content << line unless @definition.noise_line?(line)
138
+ next
139
+ end
140
+
141
+ if in_section
142
+ # Check for section end
143
+ if end_pattern && line.match?(end_pattern)
144
+ in_section = false
145
+ next
146
+ end
147
+
148
+ # Skip noise lines but keep important ones
149
+ if @definition.important_line?(line) || !@definition.noise_line?(line)
150
+ content << line
151
+ end
152
+ end
153
+ end
154
+
155
+ content.map(&:rstrip)
156
+ end
157
+
158
+ # Extract lines with error patterns (when no sections defined)
159
+ def extract_error_lines(lines)
160
+ result = []
161
+ context_buffer = []
162
+ context_lines = @definition.context_lines
163
+
164
+ lines.each_with_index do |line, index|
165
+ # Keep track of context
166
+ context_buffer << line
167
+ context_buffer.shift if context_buffer.size > context_lines
168
+
169
+ # Check for error indicators
170
+ if @definition.error_line?(line) || @definition.important_line?(line)
171
+ # Add preceding context
172
+ result.concat(context_buffer[0...-1]) if context_buffer.size > 1
173
+
174
+ # Add the error line
175
+ result << line
176
+
177
+ # Add following context
178
+ following = lines[index + 1, context_lines] || []
179
+ following.each do |following_line|
180
+ break if @definition.summary_line?(following_line)
181
+ result << following_line unless @definition.noise_line?(following_line)
182
+ end
183
+
184
+ context_buffer.clear
185
+ end
186
+ end
187
+
188
+ result.uniq.map(&:rstrip)
189
+ end
190
+
191
+ # Extract all file locations from output
192
+ def extract_all_locations(lines)
193
+ lines.flat_map { |line| @definition.extract_locations(line) }
194
+ end
195
+ end
196
+ end
197
+ end
@@ -1,9 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "output_filter_config"
4
+
3
5
  module Aidp
4
6
  module Harness
5
7
  # Filters test and linter output to reduce token consumption
6
- # Uses framework-specific strategies to extract relevant information
8
+ #
9
+ # Supports two filtering approaches:
10
+ # 1. AI-generated FilterDefinitions (preferred) - created once during config,
11
+ # applied deterministically at runtime for ANY tool
12
+ # 2. Built-in RSpec strategy (fallback) - for backward compatibility
13
+ #
14
+ # @example Using AI-generated filter
15
+ # definition = FilterDefinition.from_hash(config[:filter_definitions][:pytest])
16
+ # filter = OutputFilter.new(mode: :failures_only, filter_definition: definition)
17
+ # filtered = filter.filter(output)
18
+ #
19
+ # @see AIFilterFactory for generating filter definitions
20
+ # @see FilterDefinition for the definition format
7
21
  class OutputFilter
8
22
  # Output modes
9
23
  MODES = {
@@ -12,30 +26,32 @@ module Aidp
12
26
  minimal: :minimal # Minimal failure info + summary
13
27
  }.freeze
14
28
 
15
- # @param config [Hash] Configuration options
29
+ # @param config [OutputFilterConfig, Hash] Configuration options
16
30
  # @option config [Symbol] :mode Output mode (:full, :failures_only, :minimal)
17
31
  # @option config [Boolean] :include_context Include surrounding lines
18
32
  # @option config [Integer] :context_lines Number of context lines
19
33
  # @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!
34
+ # @param filter_definition [FilterDefinition, nil] AI-generated filter patterns
35
+ def initialize(config = {}, filter_definition: nil)
36
+ @config = normalize_config(config)
37
+ @mode = @config.mode
38
+ @include_context = @config.include_context
39
+ @context_lines = @config.context_lines
40
+ @max_lines = @config.max_lines
41
+ @filter_definition = filter_definition
27
42
 
28
43
  Aidp.log_debug("output_filter", "initialized",
29
44
  mode: @mode,
30
45
  include_context: @include_context,
31
- max_lines: @max_lines)
46
+ max_lines: @max_lines,
47
+ has_definition: !@filter_definition.nil?)
32
48
  rescue NameError
33
49
  # Logging infrastructure not available in some tests
34
50
  end
35
51
 
36
52
  # Filter output based on framework and mode
37
53
  # @param output [String] Raw output
38
- # @param framework [Symbol] Framework identifier
54
+ # @param framework [Symbol] Framework identifier (:rspec, :minitest, :jest, :pytest, :unknown)
39
55
  # @return [String] Filtered output
40
56
  def filter(output, framework: :unknown)
41
57
  return output if @mode == :full
@@ -43,6 +59,7 @@ module Aidp
43
59
 
44
60
  Aidp.log_debug("output_filter", "filtering_start",
45
61
  framework: framework,
62
+ mode: @mode,
46
63
  input_lines: output.lines.count)
47
64
 
48
65
  strategy = strategy_for_framework(framework)
@@ -51,6 +68,7 @@ module Aidp
51
68
  truncated = truncate_if_needed(filtered)
52
69
 
53
70
  Aidp.log_debug("output_filter", "filtering_complete",
71
+ framework: framework,
54
72
  output_lines: truncated.lines.count,
55
73
  reduction: reduction_stats(output, truncated))
56
74
 
@@ -80,31 +98,38 @@ module Aidp
80
98
  end
81
99
 
82
100
  # Accessors for strategy use
83
- attr_reader :mode, :include_context, :context_lines, :max_lines
101
+ attr_reader :mode, :include_context, :context_lines, :max_lines, :config, :filter_definition
84
102
 
85
103
  private
86
104
 
87
- def validate_mode!
88
- unless MODES.key?(@mode)
89
- raise ArgumentError, "Invalid mode: #{@mode}. Must be one of #{MODES.keys}"
105
+ def normalize_config(config)
106
+ case config
107
+ when OutputFilterConfig
108
+ config
109
+ when Hash
110
+ OutputFilterConfig.from_hash(config)
111
+ when nil
112
+ OutputFilterConfig.new
113
+ else
114
+ raise ArgumentError, "Config must be an OutputFilterConfig or Hash, got: #{config.class}"
90
115
  end
91
116
  end
92
117
 
93
118
  def strategy_for_framework(framework)
119
+ # Use AI-generated filter definition if available (preferred)
120
+ if @filter_definition
121
+ require_relative "generated_filter_strategy"
122
+ return GeneratedFilterStrategy.new(@filter_definition)
123
+ end
124
+
125
+ # Fall back to built-in strategies for backward compatibility
94
126
  case framework
95
127
  when :rspec
96
128
  require_relative "rspec_filter_strategy"
97
129
  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
130
  else
131
+ # Use generic strategy for all other frameworks
132
+ # Users should generate a FilterDefinition for better results
108
133
  require_relative "generic_filter_strategy"
109
134
  GenericFilterStrategy.new
110
135
  end
@@ -123,7 +148,7 @@ module Aidp
123
148
  def reduction_stats(input, output)
124
149
  input_size = input.bytesize
125
150
  output_size = output.bytesize
126
- reduction = ((input_size - output_size).to_f / input_size * 100).round(1)
151
+ reduction = input_size.zero? ? 0 : ((input_size - output_size).to_f / input_size * 100).round(1)
127
152
 
128
153
  {
129
154
  input_bytes: input_size,