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.
- checksums.yaml +4 -4
- data/README.md +69 -0
- data/lib/aidp/cli.rb +43 -2
- data/lib/aidp/config.rb +9 -14
- data/lib/aidp/execute/prompt_manager.rb +128 -1
- data/lib/aidp/execute/repl_macros.rb +555 -0
- data/lib/aidp/execute/work_loop_runner.rb +108 -1
- data/lib/aidp/harness/ai_decision_engine.rb +376 -0
- data/lib/aidp/harness/capability_registry.rb +273 -0
- data/lib/aidp/harness/config_schema.rb +305 -1
- data/lib/aidp/harness/configuration.rb +452 -0
- data/lib/aidp/harness/enhanced_runner.rb +7 -1
- data/lib/aidp/harness/provider_factory.rb +0 -2
- data/lib/aidp/harness/runner.rb +7 -1
- data/lib/aidp/harness/thinking_depth_manager.rb +335 -0
- data/lib/aidp/harness/zfc_condition_detector.rb +395 -0
- data/lib/aidp/init/devcontainer_generator.rb +274 -0
- data/lib/aidp/init/runner.rb +37 -10
- data/lib/aidp/init.rb +1 -0
- data/lib/aidp/prompt_optimization/context_composer.rb +286 -0
- data/lib/aidp/prompt_optimization/optimizer.rb +335 -0
- data/lib/aidp/prompt_optimization/prompt_builder.rb +309 -0
- data/lib/aidp/prompt_optimization/relevance_scorer.rb +256 -0
- data/lib/aidp/prompt_optimization/source_code_fragmenter.rb +308 -0
- data/lib/aidp/prompt_optimization/style_guide_indexer.rb +240 -0
- data/lib/aidp/prompt_optimization/template_indexer.rb +250 -0
- data/lib/aidp/provider_manager.rb +0 -2
- data/lib/aidp/providers/anthropic.rb +19 -0
- data/lib/aidp/setup/wizard.rb +299 -4
- data/lib/aidp/utils/devcontainer_detector.rb +166 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +72 -6
- data/lib/aidp/watch/repository_client.rb +2 -1
- data/lib/aidp.rb +0 -1
- data/templates/aidp.yml.example +128 -0
- metadata +14 -2
- 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?
         |