aidp 0.17.1 → 0.18.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -0
  3. data/lib/aidp/cli.rb +43 -2
  4. data/lib/aidp/config.rb +9 -14
  5. data/lib/aidp/execute/prompt_manager.rb +128 -1
  6. data/lib/aidp/execute/repl_macros.rb +555 -0
  7. data/lib/aidp/execute/work_loop_runner.rb +108 -1
  8. data/lib/aidp/harness/ai_decision_engine.rb +376 -0
  9. data/lib/aidp/harness/capability_registry.rb +273 -0
  10. data/lib/aidp/harness/config_schema.rb +305 -1
  11. data/lib/aidp/harness/configuration.rb +452 -0
  12. data/lib/aidp/harness/enhanced_runner.rb +7 -1
  13. data/lib/aidp/harness/provider_factory.rb +0 -2
  14. data/lib/aidp/harness/runner.rb +7 -1
  15. data/lib/aidp/harness/thinking_depth_manager.rb +335 -0
  16. data/lib/aidp/harness/zfc_condition_detector.rb +395 -0
  17. data/lib/aidp/init/devcontainer_generator.rb +274 -0
  18. data/lib/aidp/init/runner.rb +37 -10
  19. data/lib/aidp/init.rb +1 -0
  20. data/lib/aidp/prompt_optimization/context_composer.rb +286 -0
  21. data/lib/aidp/prompt_optimization/optimizer.rb +335 -0
  22. data/lib/aidp/prompt_optimization/prompt_builder.rb +309 -0
  23. data/lib/aidp/prompt_optimization/relevance_scorer.rb +256 -0
  24. data/lib/aidp/prompt_optimization/source_code_fragmenter.rb +308 -0
  25. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +240 -0
  26. data/lib/aidp/prompt_optimization/template_indexer.rb +250 -0
  27. data/lib/aidp/provider_manager.rb +0 -2
  28. data/lib/aidp/providers/anthropic.rb +19 -0
  29. data/lib/aidp/setup/wizard.rb +299 -4
  30. data/lib/aidp/utils/devcontainer_detector.rb +166 -0
  31. data/lib/aidp/version.rb +1 -1
  32. data/lib/aidp/watch/build_processor.rb +72 -6
  33. data/lib/aidp/watch/repository_client.rb +2 -1
  34. data/lib/aidp.rb +0 -1
  35. data/templates/aidp.yml.example +128 -0
  36. metadata +14 -2
  37. data/lib/aidp/providers/macos_ui.rb +0 -102
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module PromptOptimization
5
+ # Scores fragments based on relevance to current task context
6
+ #
7
+ # Calculates relevance scores (0.0-1.0) for fragments based on:
8
+ # - Task type (feature, bugfix, refactor, test)
9
+ # - Affected files and code locations
10
+ # - Work loop step (planning vs implementation)
11
+ # - Keywords and semantic similarity
12
+ #
13
+ # @example Basic usage
14
+ # scorer = RelevanceScorer.new
15
+ # context = TaskContext.new(task_type: :feature, affected_files: ["user.rb"])
16
+ # score = scorer.score_fragment(fragment, context)
17
+ class RelevanceScorer
18
+ # Default scoring weights
19
+ DEFAULT_WEIGHTS = {
20
+ task_type_match: 0.3,
21
+ tag_match: 0.25,
22
+ file_location_match: 0.25,
23
+ step_match: 0.2
24
+ }.freeze
25
+
26
+ def initialize(weights: DEFAULT_WEIGHTS)
27
+ @weights = weights
28
+ end
29
+
30
+ # Score a single fragment
31
+ #
32
+ # @param fragment [Fragment, TemplateFragment, CodeFragment] Fragment to score
33
+ # @param context [TaskContext] Task context
34
+ # @return [Float] Relevance score (0.0-1.0)
35
+ def score_fragment(fragment, context)
36
+ scores = {}
37
+
38
+ scores[:task_type] = score_task_type_match(fragment, context) * @weights[:task_type_match]
39
+ scores[:tags] = score_tag_match(fragment, context) * @weights[:tag_match]
40
+ scores[:location] = score_file_location_match(fragment, context) * @weights[:file_location_match]
41
+ scores[:step] = score_step_match(fragment, context) * @weights[:step_match]
42
+
43
+ total_score = scores.values.sum
44
+ normalize_score(total_score)
45
+ end
46
+
47
+ # Score multiple fragments
48
+ #
49
+ # @param fragments [Array] List of fragments
50
+ # @param context [TaskContext] Task context
51
+ # @return [Array<Hash>] List of {fragment:, score:, breakdown:}
52
+ def score_fragments(fragments, context)
53
+ fragments.map do |fragment|
54
+ score = score_fragment(fragment, context)
55
+ {
56
+ fragment: fragment,
57
+ score: score,
58
+ breakdown: score_breakdown(fragment, context)
59
+ }
60
+ end.sort_by { |result| -result[:score] }
61
+ end
62
+
63
+ private
64
+
65
+ # Score based on task type matching
66
+ #
67
+ # @param fragment [Fragment] Fragment to score
68
+ # @param context [TaskContext] Task context
69
+ # @return [Float] Score 0.0-1.0
70
+ def score_task_type_match(fragment, context)
71
+ return 0.5 unless context.task_type # Neutral if unknown
72
+
73
+ task_tags = task_type_to_tags(context.task_type)
74
+ return 0.3 if task_tags.empty? # Low default score
75
+
76
+ if fragment.respond_to?(:tags)
77
+ matching_tags = fragment.tags & task_tags
78
+ matching_tags.empty? ? 0.3 : (matching_tags.count.to_f / task_tags.count)
79
+ else
80
+ 0.3
81
+ end
82
+ end
83
+
84
+ # Score based on tag matching
85
+ #
86
+ # @param fragment [Fragment] Fragment to score
87
+ # @param context [TaskContext] Task context
88
+ # @return [Float] Score 0.0-1.0
89
+ def score_tag_match(fragment, context)
90
+ return 0.5 unless context.tags && !context.tags.empty?
91
+
92
+ if fragment.respond_to?(:tags)
93
+ matching_tags = fragment.tags & context.tags
94
+ matching_tags.empty? ? 0.2 : (matching_tags.count.to_f / context.tags.count).clamp(0.0, 1.0)
95
+ else
96
+ 0.5
97
+ end
98
+ end
99
+
100
+ # Score based on file location matching
101
+ #
102
+ # @param fragment [Fragment] Fragment to score
103
+ # @param context [TaskContext] Task context
104
+ # @return [Float] Score 0.0-1.0
105
+ def score_file_location_match(fragment, context)
106
+ return 0.5 unless context.affected_files && !context.affected_files.empty?
107
+
108
+ # Only code fragments have file_path
109
+ return 0.5 unless fragment.respond_to?(:file_path)
110
+
111
+ # Check if fragment is from an affected file
112
+ (context.affected_files.any? do |affected_file|
113
+ fragment.file_path.include?(affected_file)
114
+ end) ? 1.0 : 0.1
115
+ end
116
+
117
+ # Score based on work loop step
118
+ #
119
+ # @param fragment [Fragment] Fragment to score
120
+ # @param context [TaskContext] Task context
121
+ # @return [Float] Score 0.0-1.0
122
+ def score_step_match(fragment, context)
123
+ return 0.5 unless context.step_name
124
+
125
+ step_tags = step_to_tags(context.step_name)
126
+ return 0.5 if step_tags.empty?
127
+
128
+ if fragment.respond_to?(:tags)
129
+ matching_tags = fragment.tags & step_tags
130
+ matching_tags.empty? ? 0.3 : 0.8
131
+ elsif fragment.respond_to?(:category)
132
+ # Template fragments have categories
133
+ step_tags.include?(fragment.category) ? 0.9 : 0.4
134
+ else
135
+ 0.5
136
+ end
137
+ end
138
+
139
+ # Get detailed score breakdown
140
+ #
141
+ # @param fragment [Fragment] Fragment to score
142
+ # @param context [TaskContext] Task context
143
+ # @return [Hash] Score breakdown
144
+ def score_breakdown(fragment, context)
145
+ {
146
+ task_type: score_task_type_match(fragment, context),
147
+ tags: score_tag_match(fragment, context),
148
+ location: score_file_location_match(fragment, context),
149
+ step: score_step_match(fragment, context)
150
+ }
151
+ end
152
+
153
+ # Normalize score to 0.0-1.0 range
154
+ #
155
+ # @param score [Float] Raw score
156
+ # @return [Float] Normalized score
157
+ def normalize_score(score)
158
+ score.clamp(0.0, 1.0)
159
+ end
160
+
161
+ # Map task type to relevant tags
162
+ #
163
+ # @param task_type [Symbol] Task type
164
+ # @return [Array<String>] List of relevant tags
165
+ def task_type_to_tags(task_type)
166
+ case task_type
167
+ when :feature, :enhancement
168
+ ["implementation", "planning", "testing", "api"]
169
+ when :bugfix, :fix
170
+ ["testing", "error", "debugging", "logging"]
171
+ when :refactor, :refactoring
172
+ ["refactor", "architecture", "testing", "performance"]
173
+ when :test, :testing
174
+ ["testing", "analyst"]
175
+ when :documentation, :docs
176
+ ["documentation", "planning"]
177
+ when :security
178
+ ["security", "testing", "error"]
179
+ when :performance
180
+ ["performance", "testing", "refactor"]
181
+ else
182
+ []
183
+ end
184
+ end
185
+
186
+ # Map work loop step to relevant tags
187
+ #
188
+ # @param step_name [String] Step name
189
+ # @return [Array<String>] List of relevant tags
190
+ def step_to_tags(step_name)
191
+ step_lower = step_name.to_s.downcase
192
+
193
+ tags = []
194
+ tags << "planning" if step_lower.include?("plan") || step_lower.include?("design")
195
+ tags << "analysis" if step_lower.include?("analy")
196
+ tags << "implementation" if step_lower.include?("implement") || step_lower.include?("code")
197
+ tags << "testing" if step_lower.include?("test")
198
+ tags << "refactor" if step_lower.include?("refactor")
199
+ tags << "documentation" if step_lower.include?("doc")
200
+ tags << "security" if step_lower.include?("security")
201
+
202
+ tags
203
+ end
204
+ end
205
+
206
+ # Represents the context for a task
207
+ #
208
+ # Contains information about the current work being done,
209
+ # used to calculate relevance scores for fragments
210
+ class TaskContext
211
+ attr_accessor :task_type, :description, :affected_files, :step_name, :tags
212
+
213
+ # @param task_type [Symbol] Type of task (:feature, :bugfix, :refactor, etc.)
214
+ # @param description [String] Task description
215
+ # @param affected_files [Array<String>] List of files being modified
216
+ # @param step_name [String] Current work loop step name
217
+ # @param tags [Array<String>] Additional context tags
218
+ def initialize(task_type: nil, description: nil, affected_files: [], step_name: nil, tags: [])
219
+ @task_type = task_type
220
+ @description = description
221
+ @affected_files = affected_files || []
222
+ @step_name = step_name
223
+ @tags = tags || []
224
+
225
+ # Extract additional tags from description if provided
226
+ extract_tags_from_description if @description
227
+ end
228
+
229
+ # Extract relevant tags from description text
230
+ def extract_tags_from_description
231
+ return unless @description
232
+
233
+ desc_lower = @description.downcase
234
+
235
+ @tags << "testing" if /test|spec|coverage/.match?(desc_lower)
236
+ @tags << "security" if /security|auth|permission/.match?(desc_lower)
237
+ @tags << "performance" if /performance|speed|optimization/.match?(desc_lower)
238
+ @tags << "database" if /database|sql|migration/.match?(desc_lower)
239
+ @tags << "api" if /\bapi\b|endpoint|rest/.match?(desc_lower)
240
+ @tags << "ui" if /\bui\b|interface|view/.match?(desc_lower)
241
+
242
+ @tags.uniq!
243
+ end
244
+
245
+ def to_h
246
+ {
247
+ task_type: @task_type,
248
+ description: @description,
249
+ affected_files: @affected_files,
250
+ step_name: @step_name,
251
+ tags: @tags
252
+ }
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,308 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module PromptOptimization
5
+ # Fragments source code files into retrievable code units
6
+ #
7
+ # Parses Ruby source files and extracts methods, classes, modules
8
+ # along with their dependencies and imports. Each fragment can be
9
+ # independently included or excluded from prompts.
10
+ #
11
+ # @example Basic usage
12
+ # fragmenter = SourceCodeFragmenter.new(project_dir: "/path/to/project")
13
+ # fragments = fragmenter.fragment_file("lib/my_file.rb")
14
+ class SourceCodeFragmenter
15
+ attr_reader :project_dir
16
+
17
+ def initialize(project_dir:)
18
+ @project_dir = project_dir
19
+ end
20
+
21
+ # Fragment a source file into code units
22
+ #
23
+ # @param file_path [String] Path to source file (relative or absolute)
24
+ # @param context_lines [Integer] Number of context lines around code units
25
+ # @return [Array<CodeFragment>] List of code fragments
26
+ def fragment_file(file_path, context_lines: 2)
27
+ abs_path = File.absolute_path?(file_path) ? file_path : File.join(@project_dir, file_path)
28
+
29
+ return [] unless File.exist?(abs_path)
30
+ return [] unless abs_path.end_with?(".rb")
31
+
32
+ content = File.read(abs_path)
33
+ fragments = []
34
+
35
+ # Extract requires/imports as first fragment
36
+ requires = extract_requires(content)
37
+ if requires && !requires.empty?
38
+ fragments << create_requires_fragment(abs_path, requires)
39
+ end
40
+
41
+ # Extract classes and modules
42
+ fragments.concat(extract_classes_and_modules(abs_path, content))
43
+
44
+ # Extract top-level methods
45
+ fragments.concat(extract_methods(abs_path, content, context_lines: context_lines))
46
+
47
+ fragments
48
+ end
49
+
50
+ # Fragment multiple files
51
+ #
52
+ # @param file_paths [Array<String>] List of file paths
53
+ # @return [Array<CodeFragment>] All fragments from all files
54
+ def fragment_files(file_paths)
55
+ file_paths.flat_map { |path| fragment_file(path) }
56
+ end
57
+
58
+ private
59
+
60
+ # Extract require statements from content
61
+ #
62
+ # @param content [String] File content
63
+ # @return [String, nil] Combined require statements
64
+ def extract_requires(content)
65
+ lines = content.lines
66
+ require_lines = lines.select do |line|
67
+ line.strip =~ /^require(_relative)?\s+/
68
+ end
69
+
70
+ return nil if require_lines.empty?
71
+
72
+ require_lines.join
73
+ end
74
+
75
+ # Create a fragment for require statements
76
+ #
77
+ # @param file_path [String] Source file path
78
+ # @param requires [String] Require statements
79
+ # @return [CodeFragment] Requires fragment
80
+ def create_requires_fragment(file_path, requires)
81
+ CodeFragment.new(
82
+ id: "#{file_path}:requires",
83
+ file_path: file_path,
84
+ type: :requires,
85
+ name: "requires",
86
+ content: requires,
87
+ line_start: 1,
88
+ line_end: requires.lines.count
89
+ )
90
+ end
91
+
92
+ # Extract classes and modules with their methods
93
+ #
94
+ # @param file_path [String] Source file path
95
+ # @param content [String] File content
96
+ # @return [Array<CodeFragment>] Class/module fragments
97
+ def extract_classes_and_modules(file_path, content)
98
+ fragments = []
99
+ lines = content.lines
100
+
101
+ current_class = nil
102
+ class_start = nil
103
+ indent_level = 0
104
+
105
+ lines.each_with_index do |line, idx|
106
+ # Detect class/module definition
107
+ if line =~ /^(\s*)(class|module)\s+(\S+)/
108
+ current_indent = $1.length
109
+ $2
110
+ name = $3
111
+
112
+ # Save previous class if exists
113
+ if current_class && class_start
114
+ class_content = lines[class_start..idx - 1].join
115
+ fragments << create_class_fragment(file_path, current_class, class_content, class_start + 1, idx)
116
+ end
117
+
118
+ current_class = name
119
+ class_start = idx
120
+ indent_level = current_indent
121
+ elsif line =~ /^(\s*)end/ && current_class
122
+ end_indent = $1.length
123
+ if end_indent <= indent_level
124
+ # Class/module end
125
+ class_content = lines[class_start..idx].join
126
+ fragments << create_class_fragment(file_path, current_class, class_content, class_start + 1, idx + 1)
127
+ current_class = nil
128
+ class_start = nil
129
+ end
130
+ end
131
+ end
132
+
133
+ # Save last class if exists
134
+ if current_class && class_start
135
+ class_content = lines[class_start..].join
136
+ fragments << create_class_fragment(file_path, current_class, class_content, class_start + 1, lines.count)
137
+ end
138
+
139
+ fragments
140
+ end
141
+
142
+ # Create a fragment for a class/module
143
+ #
144
+ # @param file_path [String] Source file path
145
+ # @param name [String] Class/module name
146
+ # @param content [String] Class/module content
147
+ # @param line_start [Integer] Starting line number
148
+ # @param line_end [Integer] Ending line number
149
+ # @return [CodeFragment] Class fragment
150
+ def create_class_fragment(file_path, name, content, line_start, line_end)
151
+ CodeFragment.new(
152
+ id: "#{file_path}:#{name}",
153
+ file_path: file_path,
154
+ type: :class,
155
+ name: name,
156
+ content: content,
157
+ line_start: line_start,
158
+ line_end: line_end
159
+ )
160
+ end
161
+
162
+ # Extract top-level methods (not inside classes)
163
+ #
164
+ # @param file_path [String] Source file path
165
+ # @param content [String] File content
166
+ # @param context_lines [Integer] Lines of context around method
167
+ # @return [Array<CodeFragment>] Method fragments
168
+ def extract_methods(file_path, content, context_lines: 2)
169
+ fragments = []
170
+ lines = content.lines
171
+
172
+ in_class = false
173
+ method_start = nil
174
+ method_name = nil
175
+ indent_level = 0
176
+
177
+ lines.each_with_index do |line, idx|
178
+ # Track if we're inside a class
179
+ if /^(\s*)(class|module)\s+/.match?(line)
180
+ in_class = true
181
+ next
182
+ elsif line =~ /^end/ && in_class
183
+ in_class = false
184
+ next
185
+ end
186
+
187
+ # Skip methods inside classes
188
+ next if in_class
189
+
190
+ # Detect method definition
191
+ if line =~ /^(\s*)def\s+(\S+)/
192
+ method_start = [idx - context_lines, 0].max
193
+ method_name = $2
194
+ indent_level = $1.length
195
+ elsif line =~ /^(\s*)end/ && method_name
196
+ end_indent = $1.length
197
+ if end_indent <= indent_level
198
+ # Method end
199
+ method_end = [idx + context_lines, lines.count - 1].min
200
+ method_content = lines[method_start..method_end].join
201
+
202
+ fragments << CodeFragment.new(
203
+ id: "#{file_path}:#{method_name}",
204
+ file_path: file_path,
205
+ type: :method,
206
+ name: method_name,
207
+ content: method_content,
208
+ line_start: method_start + 1,
209
+ line_end: method_end + 1
210
+ )
211
+
212
+ method_start = nil
213
+ method_name = nil
214
+ end
215
+ end
216
+ end
217
+
218
+ fragments
219
+ end
220
+ end
221
+
222
+ # Represents a code fragment (class, method, requires, etc.)
223
+ #
224
+ # Each fragment is a logical unit of code that can be independently
225
+ # included or excluded from prompts based on relevance
226
+ class CodeFragment
227
+ attr_reader :id, :file_path, :type, :name, :content, :line_start, :line_end
228
+
229
+ # @param id [String] Unique identifier (e.g., "lib/user.rb:User")
230
+ # @param file_path [String] Source file path
231
+ # @param type [Symbol] Fragment type (:class, :module, :method, :requires)
232
+ # @param name [String] Name of the code unit
233
+ # @param content [String] Code content
234
+ # @param line_start [Integer] Starting line number
235
+ # @param line_end [Integer] Ending line number
236
+ def initialize(id:, file_path:, type:, name:, content:, line_start:, line_end:)
237
+ @id = id
238
+ @file_path = file_path
239
+ @type = type
240
+ @name = name
241
+ @content = content
242
+ @line_start = line_start
243
+ @line_end = line_end
244
+ end
245
+
246
+ # Get the size of the fragment in characters
247
+ #
248
+ # @return [Integer] Character count
249
+ def size
250
+ @content.length
251
+ end
252
+
253
+ # Estimate token count (rough approximation: 1 token ≈ 4 chars)
254
+ #
255
+ # @return [Integer] Estimated token count
256
+ def estimated_tokens
257
+ (size / 4.0).ceil
258
+ end
259
+
260
+ # Get line count
261
+ #
262
+ # @return [Integer] Number of lines
263
+ def line_count
264
+ @line_end - @line_start + 1
265
+ end
266
+
267
+ # Get relative file path from project root
268
+ #
269
+ # @param project_dir [String] Project directory
270
+ # @return [String] Relative path
271
+ def relative_path(project_dir)
272
+ @file_path.sub(%r{^#{Regexp.escape(project_dir)}/?}, "")
273
+ end
274
+
275
+ # Check if this is a test file fragment
276
+ #
277
+ # @return [Boolean] True if from spec file
278
+ def test_file?
279
+ !!(@file_path =~ /_(spec|test)\.rb$/)
280
+ end
281
+
282
+ # Get a summary of the fragment
283
+ #
284
+ # @return [Hash] Fragment summary
285
+ def summary
286
+ {
287
+ id: @id,
288
+ file_path: @file_path,
289
+ type: @type,
290
+ name: @name,
291
+ lines: "#{@line_start}-#{@line_end}",
292
+ line_count: line_count,
293
+ size: size,
294
+ estimated_tokens: estimated_tokens,
295
+ test_file: test_file?
296
+ }
297
+ end
298
+
299
+ def to_s
300
+ "CodeFragment<#{@type}:#{@name}>"
301
+ end
302
+
303
+ def inspect
304
+ "#<CodeFragment id=#{@id} type=#{@type} lines=#{@line_start}-#{@line_end}>"
305
+ end
306
+ end
307
+ end
308
+ end