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
@@ -4,6 +4,7 @@ require "tty-prompt"
4
4
  require_relative "../message_display"
5
5
  require_relative "project_analyzer"
6
6
  require_relative "doc_generator"
7
+ require_relative "devcontainer_generator"
7
8
 
8
9
  module Aidp
9
10
  module Init
@@ -12,11 +13,12 @@ module Aidp
12
13
  class Runner
13
14
  include Aidp::MessageDisplay
14
15
 
15
- def initialize(project_dir = Dir.pwd, prompt: TTY::Prompt.new, analyzer: nil, doc_generator: nil, options: {})
16
+ def initialize(project_dir = Dir.pwd, prompt: TTY::Prompt.new, analyzer: nil, doc_generator: nil, devcontainer_generator: nil, options: {})
16
17
  @project_dir = project_dir
17
18
  @prompt = prompt
18
19
  @analyzer = analyzer || ProjectAnalyzer.new(project_dir)
19
20
  @doc_generator = doc_generator || DocGenerator.new(project_dir)
21
+ @devcontainer_generator = devcontainer_generator || DevcontainerGenerator.new(project_dir)
20
22
  @options = options
21
23
  end
22
24
 
@@ -62,20 +64,30 @@ module Aidp
62
64
 
63
65
  @doc_generator.generate(analysis: analysis, preferences: preferences)
64
66
 
67
+ generated_files = [
68
+ "docs/LLM_STYLE_GUIDE.md",
69
+ "docs/PROJECT_ANALYSIS.md",
70
+ "docs/CODE_QUALITY_PLAN.md"
71
+ ]
72
+
65
73
  display_message("\nšŸ“„ Generated documentation:", type: :info)
66
- display_message(" - docs/LLM_STYLE_GUIDE.md", type: :success)
67
- display_message(" - docs/PROJECT_ANALYSIS.md", type: :success)
68
- display_message(" - docs/CODE_QUALITY_PLAN.md", type: :success)
74
+ generated_files.each { |file| display_message(" - #{file}", type: :success) }
75
+
76
+ # Optionally generate devcontainer
77
+ if @options[:with_devcontainer] || should_generate_devcontainer?(preferences)
78
+ devcontainer_files = @devcontainer_generator.generate(analysis: analysis, preferences: preferences)
79
+ generated_files.concat(devcontainer_files)
80
+
81
+ display_message("\nšŸ“¦ Generated devcontainer:", type: :info)
82
+ devcontainer_files.each { |file| display_message(" - #{file}", type: :success) }
83
+ end
84
+
69
85
  display_message("\nāœ… aidp init complete.", type: :success)
70
86
 
71
87
  {
72
88
  analysis: analysis,
73
89
  preferences: preferences,
74
- generated_files: [
75
- "docs/LLM_STYLE_GUIDE.md",
76
- "docs/PROJECT_ANALYSIS.md",
77
- "docs/CODE_QUALITY_PLAN.md"
78
- ]
90
+ generated_files: generated_files
79
91
  }
80
92
  end
81
93
 
@@ -225,7 +237,7 @@ module Aidp
225
237
  display_message("The following questions will help customize the generated documentation.", type: :info)
226
238
  display_message("Press Enter to accept defaults shown in brackets.\n", type: :info)
227
239
 
228
- {
240
+ prefs = {
229
241
  adopt_new_conventions: ask_yes_no_with_context(
230
242
  "Make these conventions official for this repository?",
231
243
  context: "This saves the detected patterns to LLM_STYLE_GUIDE.md and guides future AI-assisted work.",
@@ -242,6 +254,21 @@ module Aidp
242
254
  default: false
243
255
  )
244
256
  }
257
+
258
+ # Ask about devcontainer generation unless explicitly set via options
259
+ unless @options.key?(:with_devcontainer)
260
+ prefs[:generate_devcontainer] = ask_yes_no_with_context(
261
+ "Generate devcontainer configuration for sandboxed development?",
262
+ context: "Creates .devcontainer/ with Docker setup and network security for safe AI agent execution.",
263
+ default: !@devcontainer_generator.exists?
264
+ )
265
+ end
266
+
267
+ prefs
268
+ end
269
+
270
+ def should_generate_devcontainer?(preferences)
271
+ preferences[:generate_devcontainer] == true
245
272
  end
246
273
 
247
274
  def ask_yes_no_with_context(question, context:, default:)
data/lib/aidp/init.rb CHANGED
@@ -2,4 +2,5 @@
2
2
 
3
3
  require_relative "init/project_analyzer"
4
4
  require_relative "init/doc_generator"
5
+ require_relative "init/devcontainer_generator"
5
6
  require_relative "init/runner"
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module PromptOptimization
5
+ # Composes optimal context from scored fragments within token budget
6
+ #
7
+ # Selects the best combination of fragments that fits within
8
+ # the token budget while maximizing relevance and coverage.
9
+ #
10
+ # Algorithm:
11
+ # 1. Sort fragments by relevance score
12
+ # 2. Always include critical fragments (score > 0.9)
13
+ # 3. Fill remaining budget with highest-scoring fragments
14
+ # 4. Deduplicate overlapping content
15
+ # 5. Return selected fragments with statistics
16
+ #
17
+ # @example Basic usage
18
+ # composer = ContextComposer.new(max_tokens: 8000)
19
+ # selection = composer.compose(scored_fragments, thresholds: {...})
20
+ class ContextComposer
21
+ attr_reader :max_tokens
22
+
23
+ # Thresholds for different fragment types
24
+ CRITICAL_SCORE_THRESHOLD = 0.9
25
+ MINIMUM_SCORE_THRESHOLD = 0.3
26
+
27
+ def initialize(max_tokens: 16000)
28
+ @max_tokens = max_tokens
29
+ end
30
+
31
+ # Compose optimal context from scored fragments
32
+ #
33
+ # @param scored_fragments [Array<Hash>] List of {fragment:, score:, breakdown:}
34
+ # @param thresholds [Hash] Type-specific thresholds {:style_guide, :templates, :source}
35
+ # @param reserved_tokens [Integer] Tokens to reserve for task description, etc.
36
+ # @return [CompositionResult] Selected fragments and statistics
37
+ def compose(scored_fragments, thresholds: {}, reserved_tokens: 2000)
38
+ available_budget = @max_tokens - reserved_tokens
39
+
40
+ # Separate fragments by type for threshold checking
41
+ categorized = categorize_fragments(scored_fragments, thresholds)
42
+
43
+ # Start with critical fragments (always included)
44
+ selected = select_critical_fragments(categorized[:critical])
45
+ used_tokens = calculate_total_tokens(selected)
46
+
47
+ # Add high-priority fragments within budget
48
+ selected, used_tokens = add_fragments_within_budget(
49
+ selected,
50
+ categorized[:high_priority],
51
+ available_budget - used_tokens
52
+ )
53
+
54
+ # Fill remaining budget with other fragments if space allows
55
+ selected, used_tokens = add_fragments_within_budget(
56
+ selected,
57
+ categorized[:medium_priority],
58
+ available_budget - used_tokens
59
+ )
60
+
61
+ # Deduplicate if requested
62
+ selected = deduplicate_fragments(selected) if categorized[:needs_dedup]
63
+
64
+ CompositionResult.new(
65
+ selected_fragments: selected,
66
+ total_tokens: used_tokens,
67
+ budget: available_budget,
68
+ excluded_count: scored_fragments.length - selected.length,
69
+ average_score: calculate_average_score(selected)
70
+ )
71
+ end
72
+
73
+ private
74
+
75
+ # Categorize fragments by priority and type
76
+ #
77
+ # @param scored_fragments [Array<Hash>] Scored fragments
78
+ # @param thresholds [Hash] Type-specific thresholds
79
+ # @return [Hash] Categorized fragments
80
+ def categorize_fragments(scored_fragments, thresholds)
81
+ critical = []
82
+ high_priority = []
83
+ medium_priority = []
84
+
85
+ scored_fragments.each do |item|
86
+ score = item[:score]
87
+ fragment = item[:fragment]
88
+
89
+ # Critical fragments always included
90
+ if score >= CRITICAL_SCORE_THRESHOLD
91
+ critical << item
92
+ # Check type-specific thresholds
93
+ elsif meets_threshold?(fragment, score, thresholds)
94
+ high_priority << item
95
+ # Medium priority if above minimum
96
+ elsif score >= MINIMUM_SCORE_THRESHOLD
97
+ medium_priority << item
98
+ end
99
+ end
100
+
101
+ {
102
+ critical: critical,
103
+ high_priority: high_priority.sort_by { |item| -item[:score] },
104
+ medium_priority: medium_priority.sort_by { |item| -item[:score] },
105
+ needs_dedup: true
106
+ }
107
+ end
108
+
109
+ # Check if fragment meets type-specific threshold
110
+ #
111
+ # @param fragment [Fragment] Fragment to check
112
+ # @param score [Float] Relevance score
113
+ # @param thresholds [Hash] Type-specific thresholds
114
+ # @return [Boolean] True if meets threshold
115
+ def meets_threshold?(fragment, score, thresholds)
116
+ threshold = if fragment.class.name.include?("Fragment") && fragment.respond_to?(:heading)
117
+ thresholds[:style_guide] || 0.75
118
+ elsif fragment.respond_to?(:category)
119
+ thresholds[:templates] || 0.8
120
+ elsif fragment.respond_to?(:file_path) && fragment.respond_to?(:type)
121
+ thresholds[:source] || 0.7
122
+ else
123
+ 0.75
124
+ end
125
+
126
+ score >= threshold
127
+ end
128
+
129
+ # Select critical fragments (always included regardless of budget)
130
+ #
131
+ # @param critical_items [Array<Hash>] Critical scored fragments
132
+ # @return [Array<Hash>] Selected critical fragments
133
+ def select_critical_fragments(critical_items)
134
+ critical_items
135
+ end
136
+
137
+ # Add fragments within remaining budget
138
+ #
139
+ # @param selected [Array<Hash>] Already selected fragments
140
+ # @param candidates [Array<Hash>] Candidate fragments to add
141
+ # @param remaining_budget [Integer] Remaining token budget
142
+ # @return [Array] [updated_selected, tokens_used]
143
+ def add_fragments_within_budget(selected, candidates, remaining_budget)
144
+ used_tokens = 0
145
+
146
+ candidates.each do |item|
147
+ fragment_tokens = estimate_fragment_tokens(item[:fragment])
148
+
149
+ if used_tokens + fragment_tokens <= remaining_budget
150
+ selected << item
151
+ used_tokens += fragment_tokens
152
+ end
153
+ end
154
+
155
+ [selected, calculate_total_tokens(selected)]
156
+ end
157
+
158
+ # Deduplicate fragments with overlapping content
159
+ #
160
+ # @param selected [Array<Hash>] Selected fragments
161
+ # @return [Array<Hash>] Deduplicated fragments
162
+ def deduplicate_fragments(selected)
163
+ # Simple deduplication: remove fragments with identical IDs
164
+ seen_ids = Set.new
165
+ selected.select do |item|
166
+ id = item[:fragment].respond_to?(:id) ? item[:fragment].id : item[:fragment].object_id
167
+ !seen_ids.include?(id).tap { seen_ids << id }
168
+ end
169
+ end
170
+
171
+ # Estimate tokens for a fragment
172
+ #
173
+ # @param fragment [Fragment] Fragment to estimate
174
+ # @return [Integer] Estimated tokens
175
+ def estimate_fragment_tokens(fragment)
176
+ if fragment.respond_to?(:estimated_tokens)
177
+ fragment.estimated_tokens
178
+ elsif fragment.respond_to?(:content)
179
+ (fragment.content.length / 4.0).ceil
180
+ else
181
+ 100 # Default estimate
182
+ end
183
+ end
184
+
185
+ # Calculate total tokens for selected fragments
186
+ #
187
+ # @param selected [Array<Hash>] Selected fragments
188
+ # @return [Integer] Total tokens
189
+ def calculate_total_tokens(selected)
190
+ selected.sum { |item| estimate_fragment_tokens(item[:fragment]) }
191
+ end
192
+
193
+ # Calculate average score of selected fragments
194
+ #
195
+ # @param selected [Array<Hash>] Selected fragments
196
+ # @return [Float] Average score
197
+ def calculate_average_score(selected)
198
+ return 0.0 if selected.empty?
199
+
200
+ total = selected.sum { |item| item[:score] }
201
+ (total / selected.length.to_f).round(3)
202
+ end
203
+ end
204
+
205
+ # Result of context composition
206
+ #
207
+ # Contains selected fragments and composition statistics
208
+ class CompositionResult
209
+ attr_reader :selected_fragments, :total_tokens, :budget, :excluded_count, :average_score
210
+
211
+ def initialize(selected_fragments:, total_tokens:, budget:, excluded_count:, average_score:)
212
+ @selected_fragments = selected_fragments
213
+ @total_tokens = total_tokens
214
+ @budget = budget
215
+ @excluded_count = excluded_count
216
+ @average_score = average_score
217
+ end
218
+
219
+ # Calculate budget utilization percentage
220
+ #
221
+ # @return [Float] Percentage used (0.0-100.0)
222
+ def budget_utilization
223
+ return 0.0 if @budget.zero?
224
+
225
+ ((@total_tokens.to_f / @budget) * 100).round(2)
226
+ end
227
+
228
+ # Get count of selected fragments
229
+ #
230
+ # @return [Integer] Number of selected fragments
231
+ def selected_count
232
+ @selected_fragments.length
233
+ end
234
+
235
+ # Check if budget was exceeded
236
+ #
237
+ # @return [Boolean] True if over budget
238
+ def over_budget?
239
+ @total_tokens > @budget
240
+ end
241
+
242
+ # Get fragments by type
243
+ #
244
+ # @param type [Symbol] Fragment type (:style_guide, :template, :code)
245
+ # @return [Array<Hash>] Fragments of specified type
246
+ def fragments_by_type(type)
247
+ @selected_fragments.select do |item|
248
+ case type
249
+ when :style_guide
250
+ item[:fragment].class.name.include?("Fragment") && item[:fragment].respond_to?(:heading)
251
+ when :template
252
+ item[:fragment].respond_to?(:category) && !item[:fragment].respond_to?(:type)
253
+ when :code
254
+ item[:fragment].respond_to?(:type) && item[:fragment].respond_to?(:file_path)
255
+ else
256
+ false
257
+ end
258
+ end
259
+ end
260
+
261
+ # Get summary statistics
262
+ #
263
+ # @return [Hash] Composition statistics
264
+ def summary
265
+ {
266
+ selected_count: selected_count,
267
+ excluded_count: @excluded_count,
268
+ total_tokens: @total_tokens,
269
+ budget: @budget,
270
+ utilization: budget_utilization,
271
+ average_score: @average_score,
272
+ over_budget: over_budget?,
273
+ by_type: {
274
+ style_guide: fragments_by_type(:style_guide).count,
275
+ templates: fragments_by_type(:template).count,
276
+ code: fragments_by_type(:code).count
277
+ }
278
+ }
279
+ end
280
+
281
+ def to_s
282
+ "CompositionResult<#{selected_count} fragments, #{@total_tokens}/#{@budget} tokens (#{budget_utilization}%)>"
283
+ end
284
+ end
285
+ end
286
+ end