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
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "filter_definition"
5
+ require_relative "provider_factory"
6
+ require_relative "thinking_depth_manager"
7
+
8
+ module Aidp
9
+ module Harness
10
+ # AI-powered factory for generating deterministic filter definitions
11
+ #
12
+ # Uses AI ONCE during configuration to analyze a tool and generate
13
+ # regex patterns and extraction rules. The generated FilterDefinition
14
+ # is then applied deterministically at runtime without any AI calls.
15
+ #
16
+ # @example Generate a filter for pytest
17
+ # factory = AIFilterFactory.new(config)
18
+ # definition = factory.generate_filter(
19
+ # tool_name: "pytest",
20
+ # tool_command: "pytest -v",
21
+ # sample_output: "... actual pytest output ..."
22
+ # )
23
+ # # Save definition to config, use deterministically at runtime
24
+ #
25
+ # @see FilterDefinition for the generated output format
26
+ # @see GeneratedFilterStrategy for runtime application
27
+ class AIFilterFactory
28
+ GENERATION_PROMPT = <<~PROMPT
29
+ Analyze the following tool output and generate regex patterns for filtering.
30
+ The goal is to extract ONLY the important information (failures, errors, locations)
31
+ and filter out noise, so that AI assistants receive concise, actionable output.
32
+
33
+ Tool: {{tool_name}}
34
+ Command: {{tool_command}}
35
+
36
+ Sample output:
37
+ ```
38
+ {{sample_output}}
39
+ ```
40
+
41
+ Generate a filter definition with these components:
42
+
43
+ 1. summary_patterns: Regex patterns that match summary/result lines (e.g., "5 passed, 2 failed")
44
+ 2. failure_section_start: Regex pattern marking where failures section begins (if applicable)
45
+ 3. failure_section_end: Regex pattern marking where failures section ends (if applicable)
46
+ 4. error_section_start: Regex pattern for errors section start (if different from failures)
47
+ 5. error_section_end: Regex pattern for errors section end
48
+ 6. error_patterns: Regex patterns that identify error/failure indicator lines
49
+ 7. location_patterns: Regex patterns to extract file:line locations from output
50
+ 8. noise_patterns: Regex patterns for lines that should be filtered out (timestamps, progress bars, etc.)
51
+ 9. important_patterns: Regex patterns for lines that should ALWAYS be kept
52
+
53
+ Important guidelines for patterns:
54
+ - Use simple, portable regex syntax
55
+ - Escape special characters properly (dots, brackets, etc.)
56
+ - Make patterns case-insensitive where appropriate
57
+ - For location patterns, use capture groups to extract the file:line portion
58
+ - Leave fields as null/empty if not applicable to this tool
59
+
60
+ Respond with ONLY valid JSON matching this structure:
61
+ {
62
+ "tool_name": "string",
63
+ "summary_patterns": ["pattern1", "pattern2"],
64
+ "failure_section_start": "pattern or null",
65
+ "failure_section_end": "pattern or null",
66
+ "error_section_start": "pattern or null",
67
+ "error_section_end": "pattern or null",
68
+ "error_patterns": ["pattern1"],
69
+ "location_patterns": ["pattern with (capture) group"],
70
+ "noise_patterns": ["pattern1"],
71
+ "important_patterns": ["pattern1"]
72
+ }
73
+ PROMPT
74
+
75
+ # JSON schema for validating AI response
76
+ RESPONSE_SCHEMA = {
77
+ type: "object",
78
+ properties: {
79
+ tool_name: {type: "string"},
80
+ summary_patterns: {type: "array", items: {type: "string"}},
81
+ failure_section_start: {type: ["string", "null"]},
82
+ failure_section_end: {type: ["string", "null"]},
83
+ error_section_start: {type: ["string", "null"]},
84
+ error_section_end: {type: ["string", "null"]},
85
+ error_patterns: {type: "array", items: {type: "string"}},
86
+ location_patterns: {type: "array", items: {type: "string"}},
87
+ noise_patterns: {type: "array", items: {type: "string"}},
88
+ important_patterns: {type: "array", items: {type: "string"}}
89
+ },
90
+ required: ["tool_name", "summary_patterns"]
91
+ }.freeze
92
+
93
+ attr_reader :config, :provider_factory
94
+
95
+ # Initialize the AI filter factory
96
+ #
97
+ # @param config [Configuration] AIDP configuration
98
+ # @param provider_factory [ProviderFactory, nil] Optional factory for AI providers
99
+ def initialize(config, provider_factory: nil)
100
+ @config = config
101
+ @provider_factory = provider_factory || ProviderFactory.new(config)
102
+ end
103
+
104
+ # Generate a filter definition for a tool
105
+ #
106
+ # @param tool_name [String] Human-readable tool name
107
+ # @param tool_command [String] The command used to run the tool
108
+ # @param sample_output [String, nil] Sample output from the tool (for better patterns)
109
+ # @param tier [String] AI tier to use ("mini", "standard", "advanced")
110
+ # @return [FilterDefinition] Generated filter definition
111
+ # @raise [GenerationError] If AI fails to generate valid patterns
112
+ def generate_filter(tool_name:, tool_command:, sample_output: nil, tier: "mini")
113
+ Aidp.log_info("ai_filter_factory", "Generating filter definition",
114
+ tool_name: tool_name, tool_command: tool_command, tier: tier)
115
+
116
+ # Build prompt with context
117
+ prompt = build_prompt(tool_name, tool_command, sample_output)
118
+
119
+ # Get AI model for the tier
120
+ thinking_manager = ThinkingDepthManager.new(config)
121
+ provider_name, model_name, _model_data = thinking_manager.select_model_for_tier(
122
+ tier,
123
+ provider: config.respond_to?(:default_provider) ? config.default_provider : nil
124
+ )
125
+
126
+ Aidp.log_debug("ai_filter_factory", "Using AI model",
127
+ provider: provider_name, model: model_name)
128
+
129
+ # Call AI
130
+ response = call_ai(provider_name, model_name, prompt)
131
+
132
+ # Parse and validate response
133
+ definition_data = parse_response(response)
134
+ validate_patterns(definition_data)
135
+
136
+ # Create FilterDefinition
137
+ definition = FilterDefinition.new(
138
+ tool_name: definition_data[:tool_name] || tool_name,
139
+ tool_command: tool_command,
140
+ summary_patterns: definition_data[:summary_patterns] || [],
141
+ failure_section_start: definition_data[:failure_section_start],
142
+ failure_section_end: definition_data[:failure_section_end],
143
+ error_section_start: definition_data[:error_section_start],
144
+ error_section_end: definition_data[:error_section_end],
145
+ error_patterns: definition_data[:error_patterns] || [],
146
+ location_patterns: definition_data[:location_patterns] || [],
147
+ noise_patterns: definition_data[:noise_patterns] || [],
148
+ important_patterns: definition_data[:important_patterns] || [],
149
+ context_lines: 3
150
+ )
151
+
152
+ Aidp.log_info("ai_filter_factory", "Filter definition generated",
153
+ tool_name: definition.tool_name,
154
+ summary_pattern_count: definition.summary_patterns.size,
155
+ location_pattern_count: definition.location_patterns.size)
156
+
157
+ definition
158
+ rescue => e
159
+ Aidp.log_error("ai_filter_factory", "Failed to generate filter",
160
+ tool_name: tool_name, error: e.message, error_class: e.class.name)
161
+ raise GenerationError, "Failed to generate filter for #{tool_name}: #{e.message}"
162
+ end
163
+
164
+ # Generate filter from tool command by running it and capturing output
165
+ #
166
+ # @param tool_command [String] Command to run
167
+ # @param project_dir [String] Directory to run command in
168
+ # @param tier [String] AI tier to use
169
+ # @return [FilterDefinition] Generated filter definition
170
+ def generate_from_command(tool_command:, project_dir: Dir.pwd, tier: "mini")
171
+ tool_name = extract_tool_name(tool_command)
172
+
173
+ # Try to get sample output by running the command
174
+ sample_output = capture_sample_output(tool_command, project_dir)
175
+
176
+ generate_filter(
177
+ tool_name: tool_name,
178
+ tool_command: tool_command,
179
+ sample_output: sample_output,
180
+ tier: tier
181
+ )
182
+ end
183
+
184
+ private
185
+
186
+ def build_prompt(tool_name, tool_command, sample_output)
187
+ prompt = GENERATION_PROMPT.dup
188
+ prompt.gsub!("{{tool_name}}", tool_name)
189
+ prompt.gsub!("{{tool_command}}", tool_command)
190
+
191
+ if sample_output && !sample_output.empty?
192
+ # Truncate very long output
193
+ truncated = (sample_output.length > 5000) ? sample_output[0..5000] + "\n...[truncated]" : sample_output
194
+ prompt.gsub!("{{sample_output}}", truncated)
195
+ else
196
+ prompt.gsub!("{{sample_output}}", "[No sample output provided - generate common patterns for #{tool_name}]")
197
+ end
198
+
199
+ prompt
200
+ end
201
+
202
+ def call_ai(provider_name, model_name, prompt)
203
+ provider_options = {
204
+ model: model_name,
205
+ output: nil,
206
+ prompt: nil
207
+ }
208
+
209
+ provider = @provider_factory.create_provider(provider_name, provider_options)
210
+ provider.send_message(prompt: prompt, session: nil)
211
+ end
212
+
213
+ def parse_response(response)
214
+ response_text = response.is_a?(String) ? response : response.to_s
215
+
216
+ # Extract JSON from response
217
+ json_match = response_text.match(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/m) ||
218
+ response_text.match(/\{.*\}/m)
219
+
220
+ raise GenerationError, "No JSON found in AI response" unless json_match
221
+
222
+ JSON.parse(json_match[0], symbolize_names: true)
223
+ rescue JSON::ParserError => e
224
+ raise GenerationError, "Invalid JSON in AI response: #{e.message}"
225
+ end
226
+
227
+ def validate_patterns(data)
228
+ # Validate that required patterns are present
229
+ unless data[:summary_patterns]&.any?
230
+ raise GenerationError, "No summary patterns generated"
231
+ end
232
+
233
+ # Test that patterns compile
234
+ test_patterns(data[:summary_patterns], "summary")
235
+ test_patterns(data[:error_patterns], "error") if data[:error_patterns]
236
+ test_patterns(data[:location_patterns], "location") if data[:location_patterns]
237
+ test_patterns(data[:noise_patterns], "noise") if data[:noise_patterns]
238
+
239
+ test_pattern(data[:failure_section_start], "failure_section_start") if data[:failure_section_start]
240
+ test_pattern(data[:failure_section_end], "failure_section_end") if data[:failure_section_end]
241
+ end
242
+
243
+ def test_patterns(patterns, name)
244
+ Array(patterns).each_with_index do |pattern, i|
245
+ test_pattern(pattern, "#{name}[#{i}]")
246
+ end
247
+ end
248
+
249
+ def test_pattern(pattern, name)
250
+ return if pattern.nil? || pattern.empty?
251
+ Regexp.new(pattern)
252
+ rescue RegexpError => e
253
+ raise GenerationError, "Invalid regex for #{name}: #{pattern} - #{e.message}"
254
+ end
255
+
256
+ def extract_tool_name(command)
257
+ # Extract tool name from command (first word after common prefixes)
258
+ cleaned = command
259
+ .sub(/^(bundle exec|npm run|yarn|npx|python -m)\s+/, "")
260
+ .split(/\s+/)
261
+ .first
262
+
263
+ cleaned || "unknown"
264
+ end
265
+
266
+ def capture_sample_output(command, project_dir)
267
+ # Run command and capture output (with timeout)
268
+ require "open3"
269
+
270
+ stdout, stderr, _ = Open3.capture3(command, chdir: project_dir)
271
+
272
+ # Combine stdout and stderr for analysis
273
+ output = stdout + stderr
274
+ output.empty? ? nil : output
275
+ rescue => e
276
+ Aidp.log_debug("ai_filter_factory", "Failed to capture sample output",
277
+ command: command, error: e.message)
278
+ nil
279
+ end
280
+ end
281
+
282
+ # Error raised when filter generation fails
283
+ class GenerationError < StandardError; end
284
+ end
285
+ end
@@ -383,7 +383,16 @@ module Aidp
383
383
  guards: {enabled: false},
384
384
  version_control: {tool: "git", behavior: "nothing", conventional_commits: false},
385
385
  coverage: {enabled: false},
386
- interactive_testing: {enabled: false, app_type: "web"}
386
+ interactive_testing: {enabled: false, app_type: "web"},
387
+ output_filtering: {
388
+ enabled: true,
389
+ test_mode: "full",
390
+ lint_mode: "full",
391
+ test_max_lines: 500,
392
+ lint_max_lines: 300,
393
+ include_context: true,
394
+ context_lines: 3
395
+ }
387
396
  },
388
397
  properties: {
389
398
  enabled: {
@@ -796,6 +805,93 @@ module Aidp
796
805
  }
797
806
  }
798
807
  }
808
+ },
809
+ output_filtering: {
810
+ type: :hash,
811
+ required: false,
812
+ default: {
813
+ enabled: true,
814
+ test_mode: "full",
815
+ lint_mode: "full",
816
+ test_max_lines: 500,
817
+ lint_max_lines: 300,
818
+ include_context: true,
819
+ context_lines: 3,
820
+ filter_definitions: {}
821
+ },
822
+ properties: {
823
+ enabled: {
824
+ type: :boolean,
825
+ required: false,
826
+ default: true
827
+ },
828
+ test_mode: {
829
+ type: :string,
830
+ required: false,
831
+ default: "full",
832
+ enum: ["full", "failures_only", "minimal"]
833
+ },
834
+ lint_mode: {
835
+ type: :string,
836
+ required: false,
837
+ default: "full",
838
+ enum: ["full", "failures_only", "minimal"]
839
+ },
840
+ test_max_lines: {
841
+ type: :integer,
842
+ required: false,
843
+ default: 500,
844
+ min: 10,
845
+ max: 10000
846
+ },
847
+ lint_max_lines: {
848
+ type: :integer,
849
+ required: false,
850
+ default: 300,
851
+ min: 10,
852
+ max: 10000
853
+ },
854
+ include_context: {
855
+ type: :boolean,
856
+ required: false,
857
+ default: true
858
+ },
859
+ context_lines: {
860
+ type: :integer,
861
+ required: false,
862
+ default: 3,
863
+ min: 0,
864
+ max: 20
865
+ },
866
+ filter_definitions: {
867
+ type: :hash,
868
+ required: false,
869
+ default: {},
870
+ # AI-generated filter definitions for specific tools
871
+ # Keys are tool names, values are FilterDefinition data
872
+ # Generated by AIFilterFactory during configuration
873
+ pattern_properties: {
874
+ /^[a-zA-Z0-9_-]+$/ => {
875
+ type: :hash,
876
+ properties: {
877
+ tool_name: {type: :string, required: true},
878
+ tool_command: {type: :string, required: false},
879
+ summary_patterns: {type: :array, required: false, default: []},
880
+ failure_section_start: {type: :string, required: false},
881
+ failure_section_end: {type: :string, required: false},
882
+ error_section_start: {type: :string, required: false},
883
+ error_section_end: {type: :string, required: false},
884
+ error_patterns: {type: :array, required: false, default: []},
885
+ location_patterns: {type: :array, required: false, default: []},
886
+ noise_patterns: {type: :array, required: false, default: []},
887
+ important_patterns: {type: :array, required: false, default: []},
888
+ context_lines: {type: :integer, required: false, default: 3},
889
+ created_at: {type: :string, required: false}
890
+ }
891
+ }
892
+ }
893
+ }
894
+ }
799
895
  }
800
896
  }
801
897
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "yaml"
4
4
  require_relative "config_schema"
5
- require_relative "../config/paths"
5
+ require_relative "../config_paths"
6
6
 
7
7
  module Aidp
8
8
  module Harness
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../config"
4
- require_relative "../config/paths"
4
+ require_relative "../config_paths"
5
5
 
6
6
  module Aidp
7
7
  module Harness
@@ -196,24 +196,68 @@ module Aidp
196
196
  normalize_commands(work_loop_config[:documentation_commands] || [])
197
197
  end
198
198
 
199
+ # Get output filtering configuration
200
+ def output_filtering_config
201
+ work_loop_config[:output_filtering] || default_output_filtering_config
202
+ end
203
+
204
+ # Check if output filtering is enabled
205
+ def output_filtering_enabled?
206
+ output_filtering_config[:enabled] != false
207
+ end
208
+
199
209
  # Get test output mode
200
210
  def test_output_mode
201
- work_loop_config.dig(:test, :output_mode) || :full
211
+ mode = output_filtering_config[:test_mode]
212
+ mode ? mode.to_sym : :full
202
213
  end
203
214
 
204
215
  # Get max output lines for tests
205
216
  def test_max_output_lines
206
- work_loop_config.dig(:test, :max_output_lines) || 500
217
+ output_filtering_config[:test_max_lines] || 500
207
218
  end
208
219
 
209
220
  # Get lint output mode
210
221
  def lint_output_mode
211
- work_loop_config.dig(:lint, :output_mode) || :full
222
+ mode = output_filtering_config[:lint_mode]
223
+ mode ? mode.to_sym : :full
212
224
  end
213
225
 
214
226
  # Get max output lines for linters
215
227
  def lint_max_output_lines
216
- work_loop_config.dig(:lint, :max_output_lines) || 300
228
+ output_filtering_config[:lint_max_lines] || 300
229
+ end
230
+
231
+ # Get include_context setting for output filtering
232
+ def output_filtering_include_context
233
+ output_filtering_config.fetch(:include_context, true)
234
+ end
235
+
236
+ # Get context_lines setting for output filtering
237
+ def output_filtering_context_lines
238
+ output_filtering_config[:context_lines] || 3
239
+ end
240
+
241
+ # Get all filter definitions from config
242
+ # @return [Hash] Map of tool keys to filter definition hashes
243
+ def filter_definitions
244
+ output_filtering_config[:filter_definitions] || {}
245
+ end
246
+
247
+ # Get filter definition for a specific tool/category
248
+ # @param key [String, Symbol] Tool key (e.g., "unit_test", "lint")
249
+ # @return [FilterDefinition, nil] Loaded filter definition or nil
250
+ def filter_definition_for(key)
251
+ definitions = filter_definitions
252
+ definition_hash = definitions[key.to_s] || definitions[key.to_sym]
253
+ return nil unless definition_hash
254
+
255
+ require_relative "filter_definition"
256
+ FilterDefinition.from_hash(definition_hash)
257
+ rescue => e
258
+ Aidp.log_warn("configuration", "failed_to_load_filter_definition",
259
+ key: key, error: e.message)
260
+ nil
217
261
  end
218
262
 
219
263
  # Get guards configuration
@@ -1054,6 +1098,18 @@ module Aidp
1054
1098
  }
1055
1099
  end
1056
1100
 
1101
+ def default_output_filtering_config
1102
+ {
1103
+ enabled: true,
1104
+ test_mode: "full",
1105
+ lint_mode: "full",
1106
+ test_max_lines: 500,
1107
+ lint_max_lines: 300,
1108
+ include_context: true,
1109
+ context_lines: 3
1110
+ }
1111
+ end
1112
+
1057
1113
  def default_thinking_config
1058
1114
  {
1059
1115
  default_tier: "mini", # Use mini tier by default for cost optimization