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,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module PromptOptimization
5
+ # Indexes LLM_STYLE_GUIDE.md into retrievable fragments
6
+ #
7
+ # Parses the style guide markdown and creates searchable fragments
8
+ # based on headings and sections. Each fragment can be independently
9
+ # included or excluded from prompts based on relevance.
10
+ #
11
+ # @example Basic usage
12
+ # indexer = StyleGuideIndexer.new(project_dir: "/path/to/project")
13
+ # indexer.index!
14
+ # fragments = indexer.find_fragments(tags: ["testing", "naming"])
15
+ class StyleGuideIndexer
16
+ attr_reader :fragments, :project_dir
17
+
18
+ def initialize(project_dir:)
19
+ @project_dir = project_dir
20
+ @fragments = []
21
+ end
22
+
23
+ # Index the style guide file
24
+ #
25
+ # Parses the LLM_STYLE_GUIDE.md and extracts fragments
26
+ # organized by sections and headings
27
+ #
28
+ # @return [Array<Fragment>] List of indexed fragments
29
+ def index!
30
+ @fragments = []
31
+ content = read_style_guide
32
+
33
+ return @fragments if content.nil? || content.empty?
34
+
35
+ parse_fragments(content)
36
+ @fragments
37
+ end
38
+
39
+ # Find fragments matching given criteria
40
+ #
41
+ # @param tags [Array<String>] Tags to match (e.g., ["testing", "naming"])
42
+ # @param heading [String] Heading pattern to match
43
+ # @param min_level [Integer] Minimum heading level (1-6)
44
+ # @param max_level [Integer] Maximum heading level (1-6)
45
+ # @return [Array<Fragment>] Matching fragments
46
+ def find_fragments(tags: nil, heading: nil, min_level: 1, max_level: 6)
47
+ results = @fragments
48
+
49
+ if tags && !tags.empty?
50
+ results = results.select { |f| f.matches_any_tag?(tags) }
51
+ end
52
+
53
+ if heading
54
+ pattern = Regexp.new(heading, Regexp::IGNORECASE)
55
+ results = results.select { |f| f.heading =~ pattern }
56
+ end
57
+
58
+ results.select { |f| f.level.between?(min_level, max_level) }
59
+ end
60
+
61
+ # Get all unique tags from indexed fragments
62
+ #
63
+ # @return [Array<String>] List of all tags
64
+ def all_tags
65
+ @fragments.flat_map(&:tags).uniq.sort
66
+ end
67
+
68
+ # Get fragment by ID
69
+ #
70
+ # @param id [String] Fragment ID (e.g., "naming-structure")
71
+ # @return [Fragment, nil] The fragment or nil if not found
72
+ def find_by_id(id)
73
+ @fragments.find { |f| f.id == id }
74
+ end
75
+
76
+ private
77
+
78
+ # Read the LLM_STYLE_GUIDE.md file
79
+ #
80
+ # @return [String, nil] File content or nil if not found
81
+ def read_style_guide
82
+ guide_path = File.join(@project_dir, "docs", "LLM_STYLE_GUIDE.md")
83
+ return nil unless File.exist?(guide_path)
84
+
85
+ File.read(guide_path)
86
+ end
87
+
88
+ # Parse markdown content into fragments
89
+ #
90
+ # @param content [String] Markdown content
91
+ # @return [Array<Fragment>] Parsed fragments
92
+ def parse_fragments(content)
93
+ lines = content.lines
94
+ current_content = []
95
+ current_heading = nil
96
+ current_level = 0
97
+
98
+ lines.each_with_index do |line, idx|
99
+ if line.match?(/^#+\s+/)
100
+ # Save previous section if exists
101
+ save_fragment(current_heading, current_level, current_content) if current_heading
102
+
103
+ # Start new section
104
+ current_level = line.match(/^(#+)/)[1].length
105
+ current_heading = line.sub(/^#+\s+/, "").strip
106
+ current_content = [line]
107
+ elsif current_heading
108
+ current_content << line
109
+ end
110
+ end
111
+
112
+ # Save last section
113
+ save_fragment(current_heading, current_level, current_content) if current_heading
114
+ end
115
+
116
+ # Save a fragment from heading and content
117
+ #
118
+ # @param heading [String] Section heading
119
+ # @param level [Integer] Heading level (1-6)
120
+ # @param content [Array<String>] Section content lines
121
+ def save_fragment(heading, level, content)
122
+ return if heading.nil? || content.empty?
123
+
124
+ fragment = Fragment.new(
125
+ id: generate_id(heading),
126
+ heading: heading,
127
+ level: level,
128
+ content: content.join,
129
+ tags: extract_tags(heading, content.join)
130
+ )
131
+
132
+ @fragments << fragment
133
+ end
134
+
135
+ # Generate a unique ID from heading
136
+ #
137
+ # @param heading [String] Section heading
138
+ # @return [String] Fragment ID (e.g., "zero-framework-cognition-zfc")
139
+ def generate_id(heading)
140
+ heading
141
+ .downcase
142
+ .gsub(/[^a-z0-9\s-]/, "")
143
+ .gsub(/\s+/, "-").squeeze("-")
144
+ .gsub(/\A-|-\z/, "")
145
+ end
146
+
147
+ # Extract relevant tags from heading and content
148
+ #
149
+ # @param heading [String] Section heading
150
+ # @param content [String] Section content
151
+ # @return [Array<String>] List of tags
152
+ def extract_tags(heading, content)
153
+ tags = []
154
+
155
+ # Extract from heading
156
+ tags << "naming" if /naming|structure/i.match?(heading)
157
+ tags << "testing" if /test/i.match?(heading)
158
+ tags << "logging" if /log/i.match?(heading)
159
+ tags << "error" if /error|exception/i.match?(heading)
160
+ tags << "security" if /security|auth/i.match?(heading)
161
+ tags << "performance" if /performance|optim/i.match?(heading)
162
+ tags << "zfc" if /zero framework|zfc/i.match?(heading)
163
+ tags << "tty" if /tty|tui|ui/i.match?(heading)
164
+ tags << "git" if /git|version|commit/i.match?(heading)
165
+ tags << "style" if /style|format|convention/i.match?(heading)
166
+ tags << "refactor" if /refactor/i.match?(heading)
167
+ tags << "architecture" if /architect|design|pattern/i.match?(heading)
168
+
169
+ # Extract from content keywords
170
+ tags << "testing" if /rspec|test.*spec|describe.*it/i.match?(content)
171
+ tags << "async" if /async|thread|concurrent/i.match?(content)
172
+ tags << "api" if /api|endpoint|rest/i.match?(content)
173
+ tags << "database" if /database|sql|query/i.match?(content)
174
+
175
+ tags.uniq
176
+ end
177
+ end
178
+
179
+ # Represents a fragment of the style guide
180
+ #
181
+ # Each fragment corresponds to a section or subsection
182
+ # and can be independently selected for inclusion in prompts
183
+ class Fragment
184
+ attr_reader :id, :heading, :level, :content, :tags
185
+
186
+ def initialize(id:, heading:, level:, content:, tags: [])
187
+ @id = id
188
+ @heading = heading
189
+ @level = level
190
+ @content = content
191
+ @tags = tags
192
+ end
193
+
194
+ # Check if fragment matches any of the given tags
195
+ #
196
+ # @param query_tags [Array<String>] Tags to match against
197
+ # @return [Boolean] True if any tag matches
198
+ def matches_any_tag?(query_tags)
199
+ query_tags = query_tags.map(&:downcase)
200
+ @tags.any? { |tag| query_tags.include?(tag.downcase) }
201
+ end
202
+
203
+ # Get the size of the fragment in characters
204
+ #
205
+ # @return [Integer] Character count
206
+ def size
207
+ @content.length
208
+ end
209
+
210
+ # Estimate token count (rough approximation: 1 token ≈ 4 chars)
211
+ #
212
+ # @return [Integer] Estimated token count
213
+ def estimated_tokens
214
+ (size / 4.0).ceil
215
+ end
216
+
217
+ # Get a summary of the fragment
218
+ #
219
+ # @return [Hash] Fragment summary
220
+ def summary
221
+ {
222
+ id: @id,
223
+ heading: @heading,
224
+ level: @level,
225
+ tags: @tags,
226
+ size: size,
227
+ estimated_tokens: estimated_tokens
228
+ }
229
+ end
230
+
231
+ def to_s
232
+ "Fragment<#{@id}>"
233
+ end
234
+
235
+ def inspect
236
+ "#<Fragment id=#{@id} heading=\"#{@heading}\" level=#{@level} tags=#{@tags}>"
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module PromptOptimization
5
+ # Indexes step templates into retrievable fragments
6
+ #
7
+ # Parses template markdown files from the templates/ directory
8
+ # and creates searchable fragments based on template category,
9
+ # content, and keywords.
10
+ #
11
+ # @example Basic usage
12
+ # indexer = TemplateIndexer.new(project_dir: "/path/to/project")
13
+ # indexer.index!
14
+ # fragments = indexer.find_templates(category: "analysis", tags: ["testing"])
15
+ class TemplateIndexer
16
+ attr_reader :templates, :project_dir
17
+
18
+ # Template categories based on directory structure
19
+ CATEGORIES = %w[analysis planning implementation].freeze
20
+
21
+ def initialize(project_dir:)
22
+ @project_dir = project_dir
23
+ @templates = []
24
+ end
25
+
26
+ # Index all template files
27
+ #
28
+ # Scans the templates/ directory and indexes all markdown templates
29
+ #
30
+ # @return [Array<TemplateFragment>] List of indexed templates
31
+ def index!
32
+ @templates = []
33
+
34
+ CATEGORIES.each do |category|
35
+ category_dir = File.join(@project_dir, "templates", category)
36
+ next unless Dir.exist?(category_dir)
37
+
38
+ index_category(category, category_dir)
39
+ end
40
+
41
+ @templates
42
+ end
43
+
44
+ # Find templates matching given criteria
45
+ #
46
+ # @param category [String, nil] Category to filter by (e.g., "analysis", "planning")
47
+ # @param tags [Array<String>] Tags to match
48
+ # @param name [String, nil] Template name pattern to match
49
+ # @return [Array<TemplateFragment>] Matching templates
50
+ def find_templates(category: nil, tags: nil, name: nil)
51
+ results = @templates
52
+
53
+ if category
54
+ results = results.select { |t| t.category == category }
55
+ end
56
+
57
+ if tags && !tags.empty?
58
+ results = results.select { |t| t.matches_any_tag?(tags) }
59
+ end
60
+
61
+ if name
62
+ pattern = Regexp.new(name, Regexp::IGNORECASE)
63
+ results = results.select { |t| t.name =~ pattern }
64
+ end
65
+
66
+ results
67
+ end
68
+
69
+ # Get all unique tags from indexed templates
70
+ #
71
+ # @return [Array<String>] List of all tags
72
+ def all_tags
73
+ @templates.flat_map(&:tags).uniq.sort
74
+ end
75
+
76
+ # Get all categories
77
+ #
78
+ # @return [Array<String>] List of categories
79
+ def categories
80
+ @templates.map(&:category).uniq.sort
81
+ end
82
+
83
+ # Get template by ID
84
+ #
85
+ # @param id [String] Template ID
86
+ # @return [TemplateFragment, nil] The template or nil if not found
87
+ def find_by_id(id)
88
+ @templates.find { |t| t.id == id }
89
+ end
90
+
91
+ private
92
+
93
+ # Index all templates in a category
94
+ #
95
+ # @param category [String] Category name
96
+ # @param category_dir [String] Category directory path
97
+ def index_category(category, category_dir)
98
+ Dir.glob(File.join(category_dir, "*.md")).each do |file_path|
99
+ template = parse_template(category, file_path)
100
+ @templates << template if template
101
+ end
102
+ end
103
+
104
+ # Parse a template file
105
+ #
106
+ # @param category [String] Template category
107
+ # @param file_path [String] Path to template file
108
+ # @return [TemplateFragment, nil] Parsed template or nil
109
+ def parse_template(category, file_path)
110
+ content = File.read(file_path)
111
+ filename = File.basename(file_path, ".md")
112
+
113
+ # Extract title from first heading
114
+ title = extract_title(content) || titleize(filename)
115
+
116
+ # Extract tags from content and filename
117
+ tags = extract_tags(filename, content, category)
118
+
119
+ TemplateFragment.new(
120
+ id: "#{category}/#{filename}",
121
+ name: title,
122
+ category: category,
123
+ file_path: file_path,
124
+ content: content,
125
+ tags: tags
126
+ )
127
+ rescue => e
128
+ Aidp.log_error("template_indexer", "Failed to parse template",
129
+ file: file_path, error: e.message)
130
+ nil
131
+ end
132
+
133
+ # Extract title from markdown content
134
+ #
135
+ # @param content [String] Markdown content
136
+ # @return [String, nil] Extracted title
137
+ def extract_title(content)
138
+ match = content.match(/^#\s+(.+)$/)
139
+ match&.[](1)&.strip
140
+ end
141
+
142
+ # Convert filename to title case
143
+ #
144
+ # @param filename [String] Filename without extension
145
+ # @return [String] Title-cased string
146
+ def titleize(filename)
147
+ filename.split("_").map(&:capitalize).join(" ")
148
+ end
149
+
150
+ # Extract tags from template
151
+ #
152
+ # @param filename [String] Template filename
153
+ # @param content [String] Template content
154
+ # @param category [String] Template category
155
+ # @return [Array<String>] List of tags
156
+ def extract_tags(filename, content, category)
157
+ tags = [category]
158
+
159
+ # Extract from filename
160
+ tags << "testing" if /test/i.match?(filename)
161
+ tags << "refactor" if /refactor/i.match?(filename)
162
+ tags << "architecture" if /architect/i.match?(filename)
163
+ tags << "documentation" if /doc/i.match?(filename)
164
+ tags << "analysis" if /analy[sz]/i.match?(filename)
165
+ tags << "planning" if /plan|design/i.match?(filename)
166
+ tags << "implementation" if /implement/i.match?(filename)
167
+ tags << "security" if /security|auth/i.match?(filename)
168
+ tags << "performance" if /performance|optim/i.match?(filename)
169
+
170
+ # Extract from content
171
+ tags << "testing" if /test coverage|testing strategy/i.match?(content)
172
+ tags << "refactor" if /refactor|complexity/i.match?(content)
173
+ tags << "security" if /security|vulnerabilit/i.match?(content)
174
+ tags << "performance" if /performance|scalability/i.match?(content)
175
+ tags << "documentation" if /documentation|readme/i.match?(content)
176
+ tags << "database" if /database|sql|schema/i.match?(content)
177
+ tags << "api" if /\bapi\b|endpoint|rest/i.match?(content)
178
+
179
+ # Extract role-based tags
180
+ tags << "analyst" if /analyst/i.match?(content)
181
+ tags << "architect" if /architect/i.match?(content)
182
+ tags << "developer" if /developer|implementation/i.match?(content)
183
+
184
+ tags.uniq
185
+ end
186
+ end
187
+
188
+ # Represents a template fragment
189
+ #
190
+ # Each template is a complete markdown file that can be
191
+ # included or excluded from prompts based on relevance
192
+ class TemplateFragment
193
+ attr_reader :id, :name, :category, :file_path, :content, :tags
194
+
195
+ def initialize(id:, name:, category:, file_path:, content:, tags: [])
196
+ @id = id
197
+ @name = name
198
+ @category = category
199
+ @file_path = file_path
200
+ @content = content
201
+ @tags = tags
202
+ end
203
+
204
+ # Check if template matches any of the given tags
205
+ #
206
+ # @param query_tags [Array<String>] Tags to match against
207
+ # @return [Boolean] True if any tag matches
208
+ def matches_any_tag?(query_tags)
209
+ query_tags = query_tags.map(&:downcase)
210
+ @tags.any? { |tag| query_tags.include?(tag.downcase) }
211
+ end
212
+
213
+ # Get the size of the template in characters
214
+ #
215
+ # @return [Integer] Character count
216
+ def size
217
+ @content.length
218
+ end
219
+
220
+ # Estimate token count (rough approximation: 1 token ≈ 4 chars)
221
+ #
222
+ # @return [Integer] Estimated token count
223
+ def estimated_tokens
224
+ (size / 4.0).ceil
225
+ end
226
+
227
+ # Get a summary of the template
228
+ #
229
+ # @return [Hash] Template summary
230
+ def summary
231
+ {
232
+ id: @id,
233
+ name: @name,
234
+ category: @category,
235
+ tags: @tags,
236
+ size: size,
237
+ estimated_tokens: estimated_tokens
238
+ }
239
+ end
240
+
241
+ def to_s
242
+ "TemplateFragment<#{@id}>"
243
+ end
244
+
245
+ def inspect
246
+ "#<TemplateFragment id=#{@id} name=\"#{@name}\" category=#{@category} tags=#{@tags}>"
247
+ end
248
+ end
249
+ end
250
+ end
@@ -142,8 +142,6 @@ module Aidp
142
142
  Aidp::Providers::Anthropic.new(prompt: prompt)
143
143
  when "gemini"
144
144
  Aidp::Providers::Gemini.new(prompt: prompt)
145
- when "macos_ui"
146
- Aidp::Providers::MacOSUI.new(prompt: prompt)
147
145
  when "github_copilot"
148
146
  Aidp::Providers::GithubCopilot.new(prompt: prompt)
149
147
  when "codex"
@@ -67,6 +67,12 @@ module Aidp
67
67
  args += ["--output-format=text"]
68
68
  end
69
69
 
70
+ # Check if we should skip permissions (devcontainer support)
71
+ if should_skip_permissions?
72
+ args << "--dangerously-skip-permissions"
73
+ debug_log("🔓 Running with elevated permissions (devcontainer mode)", level: :info)
74
+ end
75
+
70
76
  begin
71
77
  # Use debug_execute_command with streaming support
72
78
  result = debug_execute_command("claude", args: args, input: prompt, timeout: timeout_seconds, streaming: streaming_enabled)
@@ -156,6 +162,19 @@ module Aidp
156
162
  end
157
163
  end
158
164
 
165
+ # Check if we should skip permissions based on devcontainer configuration
166
+ def should_skip_permissions?
167
+ # Check if harness context is available
168
+ return false unless @harness_context
169
+
170
+ # Get configuration from harness
171
+ config = @harness_context.config
172
+ return false unless config
173
+
174
+ # Use configuration method to determine if full permissions should be used
175
+ config.should_use_full_permissions?("claude")
176
+ end
177
+
159
178
  # Parse stream-json output from Claude CLI
160
179
  def parse_stream_json_output(output)
161
180
  return output if output.nil? || output.empty?