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,395 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "condition_detector"
         | 
| 4 | 
            +
            require_relative "ai_decision_engine"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Aidp
         | 
| 7 | 
            +
              module Harness
         | 
| 8 | 
            +
                # ZFC-enabled wrapper for ConditionDetector
         | 
| 9 | 
            +
                #
         | 
| 10 | 
            +
                # Delegates semantic analysis to AI when ZFC is enabled, falls back to
         | 
| 11 | 
            +
                # legacy pattern matching when disabled or on AI failure.
         | 
| 12 | 
            +
                #
         | 
| 13 | 
            +
                # @example Basic usage
         | 
| 14 | 
            +
                #   detector = ZfcConditionDetector.new(config, provider_factory)
         | 
| 15 | 
            +
                #   if detector.is_rate_limited?(result)
         | 
| 16 | 
            +
                #     # Handle rate limit
         | 
| 17 | 
            +
                #   end
         | 
| 18 | 
            +
                #
         | 
| 19 | 
            +
                # @see docs/ZFC_COMPLIANCE_ASSESSMENT.md
         | 
| 20 | 
            +
                # @see docs/ZFC_IMPLEMENTATION_PLAN.md
         | 
| 21 | 
            +
                class ZfcConditionDetector
         | 
| 22 | 
            +
                  attr_reader :config, :legacy_detector, :ai_engine, :stats
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  # Initialize ZFC condition detector
         | 
| 25 | 
            +
                  #
         | 
| 26 | 
            +
                  # @param config [Configuration, ConfigManager] AIDP configuration
         | 
| 27 | 
            +
                  # @param provider_factory [ProviderFactory, nil] Optional factory for creating providers
         | 
| 28 | 
            +
                  def initialize(config, provider_factory: nil)
         | 
| 29 | 
            +
                    @config = config
         | 
| 30 | 
            +
                    @legacy_detector = ConditionDetector.new
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    # Create ProviderFactory if not provided and ZFC is enabled
         | 
| 33 | 
            +
                    # Note: ConfigManager doesn't have zfc_enabled?, so we check respond_to? first
         | 
| 34 | 
            +
                    if provider_factory.nil? && config.respond_to?(:zfc_enabled?) && config.zfc_enabled?
         | 
| 35 | 
            +
                      require_relative "provider_factory"
         | 
| 36 | 
            +
                      provider_factory = ProviderFactory.new
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    @ai_engine = AIDecisionEngine.new(config, provider_factory: provider_factory)
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    # Statistics for A/B testing
         | 
| 42 | 
            +
                    @stats = {
         | 
| 43 | 
            +
                      zfc_calls: 0,
         | 
| 44 | 
            +
                      legacy_calls: 0,
         | 
| 45 | 
            +
                      zfc_fallbacks: 0,
         | 
| 46 | 
            +
                      agreements: 0,
         | 
| 47 | 
            +
                      disagreements: 0,
         | 
| 48 | 
            +
                      zfc_total_cost: 0.0
         | 
| 49 | 
            +
                    }
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  # Check if result indicates rate limiting
         | 
| 53 | 
            +
                  #
         | 
| 54 | 
            +
                  # @param result [Hash] AI response or error
         | 
| 55 | 
            +
                  # @param provider [String, nil] Provider name for context
         | 
| 56 | 
            +
                  # @return [Boolean] true if rate limited
         | 
| 57 | 
            +
                  def is_rate_limited?(result, provider = nil)
         | 
| 58 | 
            +
                    detect_condition(:is_rate_limited?, result, provider: provider) do |ai_result|
         | 
| 59 | 
            +
                      ai_result[:condition] == "rate_limit" &&
         | 
| 60 | 
            +
                        ai_result[:confidence] >= confidence_threshold(:condition_detection)
         | 
| 61 | 
            +
                    end
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  # Check if result needs user feedback
         | 
| 65 | 
            +
                  #
         | 
| 66 | 
            +
                  # @param result [Hash] AI response
         | 
| 67 | 
            +
                  # @return [Boolean] true if user feedback needed
         | 
| 68 | 
            +
                  def needs_user_feedback?(result)
         | 
| 69 | 
            +
                    detect_condition(:needs_user_feedback?, result, provider: nil) do |ai_result|
         | 
| 70 | 
            +
                      ai_result[:condition] == "user_feedback_needed" &&
         | 
| 71 | 
            +
                        ai_result[:confidence] >= confidence_threshold(:condition_detection)
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  # Check if work is complete
         | 
| 76 | 
            +
                  #
         | 
| 77 | 
            +
                  # @param result [Hash] AI response
         | 
| 78 | 
            +
                  # @param progress [Hash, nil] Progress context
         | 
| 79 | 
            +
                  # @return [Boolean] true if work complete
         | 
| 80 | 
            +
                  def is_work_complete?(result, progress = nil)
         | 
| 81 | 
            +
                    return false unless result
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                    if zfc_enabled?(:completion_detection)
         | 
| 84 | 
            +
                      begin
         | 
| 85 | 
            +
                        # Build context for AI decision
         | 
| 86 | 
            +
                        context = {
         | 
| 87 | 
            +
                          response: result_to_text(result),
         | 
| 88 | 
            +
                          task_description: progress&.dig(:task) || "general task"
         | 
| 89 | 
            +
                        }
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                        # Ask AI if work is complete
         | 
| 92 | 
            +
                        ai_result = @ai_engine.decide(:completion_detection,
         | 
| 93 | 
            +
                          context: context,
         | 
| 94 | 
            +
                          tier: zfc_tier(:completion_detection),
         | 
| 95 | 
            +
                          cache_ttl: zfc_cache_ttl(:completion_detection))
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                        record_zfc_call(:completion_detection, ai_result)
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                        # A/B test if enabled
         | 
| 100 | 
            +
                        if ab_testing_enabled?
         | 
| 101 | 
            +
                          legacy_result = @legacy_detector.is_work_complete?(result, progress)
         | 
| 102 | 
            +
                          compare_results(:is_work_complete, ai_result[:complete], legacy_result)
         | 
| 103 | 
            +
                        end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                        ai_result[:complete] &&
         | 
| 106 | 
            +
                          ai_result[:confidence] >= confidence_threshold(:completion_detection)
         | 
| 107 | 
            +
                      rescue => e
         | 
| 108 | 
            +
                        Aidp.log_error("zfc_condition_detector", "ZFC completion detection failed, falling back to legacy", {
         | 
| 109 | 
            +
                          error: e.message,
         | 
| 110 | 
            +
                          error_class: e.class.name
         | 
| 111 | 
            +
                        })
         | 
| 112 | 
            +
                        record_fallback(:completion_detection)
         | 
| 113 | 
            +
                        @legacy_detector.is_work_complete?(result, progress)
         | 
| 114 | 
            +
                      end
         | 
| 115 | 
            +
                    else
         | 
| 116 | 
            +
                      @stats[:legacy_calls] += 1
         | 
| 117 | 
            +
                      @legacy_detector.is_work_complete?(result, progress)
         | 
| 118 | 
            +
                    end
         | 
| 119 | 
            +
                  end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  # Extract questions from result (delegates to legacy for now)
         | 
| 122 | 
            +
                  #
         | 
| 123 | 
            +
                  # @param result [Hash] AI response
         | 
| 124 | 
            +
                  # @return [Array<Hash>] List of questions
         | 
| 125 | 
            +
                  def extract_questions(result)
         | 
| 126 | 
            +
                    @legacy_detector.extract_questions(result)
         | 
| 127 | 
            +
                  end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                  # Extract rate limit info (delegates to legacy for now)
         | 
| 130 | 
            +
                  #
         | 
| 131 | 
            +
                  # @param result [Hash] AI response or error
         | 
| 132 | 
            +
                  # @param provider [String, nil] Provider name
         | 
| 133 | 
            +
                  # @return [Hash] Rate limit information
         | 
| 134 | 
            +
                  def extract_rate_limit_info(result, provider = nil)
         | 
| 135 | 
            +
                    @legacy_detector.extract_rate_limit_info(result, provider)
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  # Classify error using AI or legacy pattern matching
         | 
| 139 | 
            +
                  #
         | 
| 140 | 
            +
                  # @param error [Exception, StandardError] Error to classify
         | 
| 141 | 
            +
                  # @param context [Hash] Additional context (provider, model, etc.)
         | 
| 142 | 
            +
                  # @return [Hash] Classification with error_type, retryable, recommended_action
         | 
| 143 | 
            +
                  def classify_error(error, context = {})
         | 
| 144 | 
            +
                    return @legacy_detector.classify_error(error) unless zfc_enabled?(:error_classification)
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                    begin
         | 
| 147 | 
            +
                      # Build context for AI decision
         | 
| 148 | 
            +
                      error_context = {
         | 
| 149 | 
            +
                        error_message: error_to_text(error),
         | 
| 150 | 
            +
                        context: context.to_s
         | 
| 151 | 
            +
                      }
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                      # Ask AI to classify error
         | 
| 154 | 
            +
                      ai_result = @ai_engine.decide(:error_classification,
         | 
| 155 | 
            +
                        context: error_context,
         | 
| 156 | 
            +
                        tier: zfc_tier(:error_classification),
         | 
| 157 | 
            +
                        cache_ttl: zfc_cache_ttl(:error_classification))
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                      record_zfc_call(:error_classification, ai_result)
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                      # A/B test if enabled
         | 
| 162 | 
            +
                      if ab_testing_enabled?
         | 
| 163 | 
            +
                        legacy_result = @legacy_detector.classify_error(error)
         | 
| 164 | 
            +
                        compare_error_results(ai_result, legacy_result)
         | 
| 165 | 
            +
                      end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                      # Only use AI result if confidence is high enough
         | 
| 168 | 
            +
                      if ai_result[:confidence] >= confidence_threshold(:error_classification)
         | 
| 169 | 
            +
                        # Convert AI result to legacy format
         | 
| 170 | 
            +
                        {
         | 
| 171 | 
            +
                          error: error,
         | 
| 172 | 
            +
                          error_type: ai_result[:error_type].to_sym,
         | 
| 173 | 
            +
                          retryable: ai_result[:retryable],
         | 
| 174 | 
            +
                          recommended_action: ai_result[:recommended_action].to_sym,
         | 
| 175 | 
            +
                          confidence: ai_result[:confidence],
         | 
| 176 | 
            +
                          reasoning: ai_result[:reasoning],
         | 
| 177 | 
            +
                          timestamp: Time.now,
         | 
| 178 | 
            +
                          context: context,
         | 
| 179 | 
            +
                          message: error&.message || "Unknown error"
         | 
| 180 | 
            +
                        }
         | 
| 181 | 
            +
                      else
         | 
| 182 | 
            +
                        @legacy_detector.classify_error(error)
         | 
| 183 | 
            +
                      end
         | 
| 184 | 
            +
                    rescue => e
         | 
| 185 | 
            +
                      Aidp.log_error("zfc_condition_detector", "ZFC error classification failed, falling back to legacy", {
         | 
| 186 | 
            +
                        error: e.message,
         | 
| 187 | 
            +
                        error_class: e.class.name,
         | 
| 188 | 
            +
                        original_error: error&.class&.name
         | 
| 189 | 
            +
                      })
         | 
| 190 | 
            +
                      record_fallback(:error_classification)
         | 
| 191 | 
            +
                      @legacy_detector.classify_error(error)
         | 
| 192 | 
            +
                    end
         | 
| 193 | 
            +
                  end
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                  # Get statistics summary
         | 
| 196 | 
            +
                  #
         | 
| 197 | 
            +
                  # @return [Hash] Statistics including accuracy, cost, performance
         | 
| 198 | 
            +
                  def statistics
         | 
| 199 | 
            +
                    total_calls = @stats[:zfc_calls] + @stats[:legacy_calls]
         | 
| 200 | 
            +
                    return @stats.merge(total_calls: 0, accuracy: nil) if total_calls.zero?
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                    comparisons = @stats[:agreements] + @stats[:disagreements]
         | 
| 203 | 
            +
                    accuracy = comparisons.zero? ? nil : (@stats[:agreements].to_f / comparisons * 100).round(2)
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                    @stats.merge(
         | 
| 206 | 
            +
                      total_calls: total_calls,
         | 
| 207 | 
            +
                      zfc_percentage: (@stats[:zfc_calls].to_f / total_calls * 100).round(2),
         | 
| 208 | 
            +
                      accuracy: accuracy,
         | 
| 209 | 
            +
                      fallback_rate: (@stats[:zfc_fallbacks].to_f / [@stats[:zfc_calls], 1].max * 100).round(2)
         | 
| 210 | 
            +
                    )
         | 
| 211 | 
            +
                  end
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                  private
         | 
| 214 | 
            +
             | 
| 215 | 
            +
                  # Generic condition detection with ZFC/legacy fallback
         | 
| 216 | 
            +
                  def detect_condition(method_name, result, provider: nil)
         | 
| 217 | 
            +
                    return false unless result
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                    if zfc_enabled?(:condition_detection)
         | 
| 220 | 
            +
                      begin
         | 
| 221 | 
            +
                        # Build context for AI decision
         | 
| 222 | 
            +
                        context = {
         | 
| 223 | 
            +
                          response: result_to_text(result)
         | 
| 224 | 
            +
                        }
         | 
| 225 | 
            +
                        context[:provider] = provider if provider
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                        # Ask AI to classify condition
         | 
| 228 | 
            +
                        ai_result = @ai_engine.decide(:condition_detection,
         | 
| 229 | 
            +
                          context: context,
         | 
| 230 | 
            +
                          tier: zfc_tier(:condition_detection),
         | 
| 231 | 
            +
                          cache_ttl: zfc_cache_ttl(:condition_detection))
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                        record_zfc_call(:condition_detection, ai_result)
         | 
| 234 | 
            +
             | 
| 235 | 
            +
                        # Convert AI result to boolean using provided block
         | 
| 236 | 
            +
                        zfc_decision = yield(ai_result)
         | 
| 237 | 
            +
             | 
| 238 | 
            +
                        # A/B test if enabled
         | 
| 239 | 
            +
                        if ab_testing_enabled?
         | 
| 240 | 
            +
                          # Call legacy method with appropriate arguments
         | 
| 241 | 
            +
                          legacy_decision = call_legacy_method(method_name, result, provider)
         | 
| 242 | 
            +
                          compare_results(method_name, zfc_decision, legacy_decision)
         | 
| 243 | 
            +
                        end
         | 
| 244 | 
            +
             | 
| 245 | 
            +
                        zfc_decision
         | 
| 246 | 
            +
                      rescue => e
         | 
| 247 | 
            +
                        Aidp.log_error("zfc_condition_detector", "ZFC condition detection failed, falling back to legacy", {
         | 
| 248 | 
            +
                          error: e.message,
         | 
| 249 | 
            +
                          error_class: e.class.name,
         | 
| 250 | 
            +
                          method: method_name
         | 
| 251 | 
            +
                        })
         | 
| 252 | 
            +
                        record_fallback(:condition_detection)
         | 
| 253 | 
            +
                        call_legacy_method(method_name, result, provider)
         | 
| 254 | 
            +
                      end
         | 
| 255 | 
            +
                    else
         | 
| 256 | 
            +
                      @stats[:legacy_calls] += 1
         | 
| 257 | 
            +
                      call_legacy_method(method_name, result, provider)
         | 
| 258 | 
            +
                    end
         | 
| 259 | 
            +
                  end
         | 
| 260 | 
            +
             | 
| 261 | 
            +
                  # Call legacy method with appropriate arguments based on method signature
         | 
| 262 | 
            +
                  def call_legacy_method(method_name, result, provider)
         | 
| 263 | 
            +
                    case method_name
         | 
| 264 | 
            +
                    when :is_rate_limited?
         | 
| 265 | 
            +
                      @legacy_detector.send(method_name, result, provider)
         | 
| 266 | 
            +
                    when :needs_user_feedback?
         | 
| 267 | 
            +
                      @legacy_detector.send(method_name, result)
         | 
| 268 | 
            +
                    else
         | 
| 269 | 
            +
                      @legacy_detector.send(method_name, result, provider)
         | 
| 270 | 
            +
                    end
         | 
| 271 | 
            +
                  end
         | 
| 272 | 
            +
             | 
| 273 | 
            +
                  # Convert result to text for AI analysis
         | 
| 274 | 
            +
                  def result_to_text(result)
         | 
| 275 | 
            +
                    case result
         | 
| 276 | 
            +
                    when String
         | 
| 277 | 
            +
                      result
         | 
| 278 | 
            +
                    when Hash
         | 
| 279 | 
            +
                      # Try common keys - match what ConditionDetector uses
         | 
| 280 | 
            +
                      result[:output] || result[:content] || result[:message] || result[:error] || result[:response] || result.to_s
         | 
| 281 | 
            +
                    else
         | 
| 282 | 
            +
                      result.to_s
         | 
| 283 | 
            +
                    end
         | 
| 284 | 
            +
                  end
         | 
| 285 | 
            +
             | 
| 286 | 
            +
                  # Convert error to text for AI analysis
         | 
| 287 | 
            +
                  def error_to_text(error)
         | 
| 288 | 
            +
                    return "Unknown error" unless error
         | 
| 289 | 
            +
             | 
| 290 | 
            +
                    message = error.message || error.to_s
         | 
| 291 | 
            +
                    error_class = error.class.name
         | 
| 292 | 
            +
             | 
| 293 | 
            +
                    # Include error class and message
         | 
| 294 | 
            +
                    text = "#{error_class}: #{message}"
         | 
| 295 | 
            +
             | 
| 296 | 
            +
                    # Add backtrace context if available
         | 
| 297 | 
            +
                    if error.backtrace && !error.backtrace.empty?
         | 
| 298 | 
            +
                      text += "\nLocation: #{error.backtrace.first}"
         | 
| 299 | 
            +
                    end
         | 
| 300 | 
            +
             | 
| 301 | 
            +
                    text
         | 
| 302 | 
            +
                  end
         | 
| 303 | 
            +
             | 
| 304 | 
            +
                  # Check if ZFC is enabled for decision type
         | 
| 305 | 
            +
                  def zfc_enabled?(decision_type)
         | 
| 306 | 
            +
                    return false unless @config.respond_to?(:zfc_decision_enabled?)
         | 
| 307 | 
            +
                    @config.zfc_decision_enabled?(decision_type)
         | 
| 308 | 
            +
                  end
         | 
| 309 | 
            +
             | 
| 310 | 
            +
                  # Get tier for ZFC decision type
         | 
| 311 | 
            +
                  def zfc_tier(decision_type)
         | 
| 312 | 
            +
                    return "mini" unless @config.respond_to?(:zfc_decision_tier)
         | 
| 313 | 
            +
                    @config.zfc_decision_tier(decision_type)
         | 
| 314 | 
            +
                  end
         | 
| 315 | 
            +
             | 
| 316 | 
            +
                  # Get cache TTL for decision type
         | 
| 317 | 
            +
                  def zfc_cache_ttl(decision_type)
         | 
| 318 | 
            +
                    return nil unless @config.respond_to?(:zfc_decision_cache_ttl)
         | 
| 319 | 
            +
                    @config.zfc_decision_cache_ttl(decision_type)
         | 
| 320 | 
            +
                  end
         | 
| 321 | 
            +
             | 
| 322 | 
            +
                  # Get confidence threshold for decision type
         | 
| 323 | 
            +
                  def confidence_threshold(decision_type)
         | 
| 324 | 
            +
                    return 0.7 unless @config.respond_to?(:zfc_decision_confidence_threshold)
         | 
| 325 | 
            +
                    @config.zfc_decision_confidence_threshold(decision_type)
         | 
| 326 | 
            +
                  end
         | 
| 327 | 
            +
             | 
| 328 | 
            +
                  # Check if A/B testing is enabled
         | 
| 329 | 
            +
                  def ab_testing_enabled?
         | 
| 330 | 
            +
                    return false unless @config.respond_to?(:zfc_ab_testing_enabled?)
         | 
| 331 | 
            +
                    @config.zfc_ab_testing_enabled?
         | 
| 332 | 
            +
                  end
         | 
| 333 | 
            +
             | 
| 334 | 
            +
                  # Record ZFC call for statistics
         | 
| 335 | 
            +
                  def record_zfc_call(decision_type, ai_result)
         | 
| 336 | 
            +
                    @stats[:zfc_calls] += 1
         | 
| 337 | 
            +
             | 
| 338 | 
            +
                    # Estimate cost (very rough)
         | 
| 339 | 
            +
                    # Mini tier: ~$0.15/MTok input, $0.75/MTok output
         | 
| 340 | 
            +
                    # Assume ~500 tokens input, ~100 tokens output per decision
         | 
| 341 | 
            +
                    estimated_cost = (500 * 0.15 / 1_000_000) + (100 * 0.75 / 1_000_000)
         | 
| 342 | 
            +
                    @stats[:zfc_total_cost] += estimated_cost
         | 
| 343 | 
            +
                  end
         | 
| 344 | 
            +
             | 
| 345 | 
            +
                  # Record fallback to legacy
         | 
| 346 | 
            +
                  def record_fallback(decision_type)
         | 
| 347 | 
            +
                    @stats[:zfc_fallbacks] += 1
         | 
| 348 | 
            +
                  end
         | 
| 349 | 
            +
             | 
| 350 | 
            +
                  # Compare ZFC vs legacy results for A/B testing
         | 
| 351 | 
            +
                  def compare_results(method_name, zfc_result, legacy_result)
         | 
| 352 | 
            +
                    if zfc_result == legacy_result
         | 
| 353 | 
            +
                      @stats[:agreements] += 1
         | 
| 354 | 
            +
                    else
         | 
| 355 | 
            +
                      @stats[:disagreements] += 1
         | 
| 356 | 
            +
             | 
| 357 | 
            +
                      # Log comparisons if configured (only available in Configuration, not ConfigManager)
         | 
| 358 | 
            +
                      if @config.respond_to?(:zfc_ab_testing_config) &&
         | 
| 359 | 
            +
                          @config.zfc_ab_testing_config[:log_comparisons]
         | 
| 360 | 
            +
                        Aidp.log_debug("zfc_ab_testing", "ZFC vs Legacy disagreement", {
         | 
| 361 | 
            +
                          method: method_name,
         | 
| 362 | 
            +
                          zfc_result: zfc_result,
         | 
| 363 | 
            +
                          legacy_result: legacy_result
         | 
| 364 | 
            +
                        })
         | 
| 365 | 
            +
                      end
         | 
| 366 | 
            +
                    end
         | 
| 367 | 
            +
                  end
         | 
| 368 | 
            +
             | 
| 369 | 
            +
                  # Compare ZFC vs legacy error classification results
         | 
| 370 | 
            +
                  def compare_error_results(ai_result, legacy_result)
         | 
| 371 | 
            +
                    # Extract comparable fields
         | 
| 372 | 
            +
                    ai_error_type = ai_result[:error_type].to_sym
         | 
| 373 | 
            +
                    legacy_error_type = legacy_result[:error_type]
         | 
| 374 | 
            +
             | 
| 375 | 
            +
                    if ai_error_type == legacy_error_type
         | 
| 376 | 
            +
                      @stats[:agreements] += 1
         | 
| 377 | 
            +
                    else
         | 
| 378 | 
            +
                      @stats[:disagreements] += 1
         | 
| 379 | 
            +
             | 
| 380 | 
            +
                      # Log comparisons if configured
         | 
| 381 | 
            +
                      if @config.respond_to?(:zfc_ab_testing_config) &&
         | 
| 382 | 
            +
                          @config.zfc_ab_testing_config[:log_comparisons]
         | 
| 383 | 
            +
                        Aidp.log_debug("zfc_ab_testing", "Error classification disagreement", {
         | 
| 384 | 
            +
                          ai_error_type: ai_error_type,
         | 
| 385 | 
            +
                          legacy_error_type: legacy_error_type,
         | 
| 386 | 
            +
                          ai_retryable: ai_result[:retryable],
         | 
| 387 | 
            +
                          ai_action: ai_result[:recommended_action],
         | 
| 388 | 
            +
                          ai_confidence: ai_result[:confidence]
         | 
| 389 | 
            +
                        })
         | 
| 390 | 
            +
                      end
         | 
| 391 | 
            +
                    end
         | 
| 392 | 
            +
                  end
         | 
| 393 | 
            +
                end
         | 
| 394 | 
            +
              end
         | 
| 395 | 
            +
            end
         | 
| @@ -0,0 +1,274 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "fileutils"
         | 
| 4 | 
            +
            require "json"
         | 
| 5 | 
            +
            require_relative "../message_display"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Aidp
         | 
| 8 | 
            +
              module Init
         | 
| 9 | 
            +
                # Generates .devcontainer configuration for projects
         | 
| 10 | 
            +
                # Provides sandboxed development environment with network security
         | 
| 11 | 
            +
                #
         | 
| 12 | 
            +
                # Design Philosophy:
         | 
| 13 | 
            +
                # - Use project analysis data (already collected by ProjectAnalyzer)
         | 
| 14 | 
            +
                # - Avoid hardcoded framework/tool assumptions
         | 
| 15 | 
            +
                # - Prefer templates over code generation
         | 
| 16 | 
            +
                # - Let the data drive decisions, not hardcoded logic
         | 
| 17 | 
            +
                class DevcontainerGenerator
         | 
| 18 | 
            +
                  include Aidp::MessageDisplay
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def initialize(project_dir = Dir.pwd)
         | 
| 21 | 
            +
                    @project_dir = project_dir
         | 
| 22 | 
            +
                    @devcontainer_dir = File.join(@project_dir, ".devcontainer")
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  # Generate devcontainer configuration based on project analysis
         | 
| 26 | 
            +
                  #
         | 
| 27 | 
            +
                  # @param analysis [Hash] Project analysis from ProjectAnalyzer
         | 
| 28 | 
            +
                  # @param preferences [Hash] User preferences for devcontainer setup
         | 
| 29 | 
            +
                  # @return [Array<String>] List of generated files
         | 
| 30 | 
            +
                  def generate(analysis:, preferences: {})
         | 
| 31 | 
            +
                    ensure_directory_exists
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    files = []
         | 
| 34 | 
            +
                    files << generate_dockerfile(analysis, preferences)
         | 
| 35 | 
            +
                    files << generate_devcontainer_json(analysis, preferences)
         | 
| 36 | 
            +
                    files << generate_firewall_script(analysis, preferences)
         | 
| 37 | 
            +
                    files << generate_readme(analysis, preferences)
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    files.compact
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  # Check if devcontainer already exists
         | 
| 43 | 
            +
                  #
         | 
| 44 | 
            +
                  # @return [Boolean] true if .devcontainer directory exists
         | 
| 45 | 
            +
                  def exists?
         | 
| 46 | 
            +
                    Dir.exist?(@devcontainer_dir)
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  private
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def ensure_directory_exists
         | 
| 52 | 
            +
                    FileUtils.mkdir_p(@devcontainer_dir) unless Dir.exist?(@devcontainer_dir)
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def generate_dockerfile(analysis, _preferences)
         | 
| 56 | 
            +
                    # Use AIDP's template as the default (Ruby-based)
         | 
| 57 | 
            +
                    # Projects can customize after generation
         | 
| 58 | 
            +
                    template_path = File.join(File.dirname(__FILE__), "..", "..", "..", ".devcontainer", "Dockerfile")
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                    content = if File.exist?(template_path)
         | 
| 61 | 
            +
                      File.read(template_path)
         | 
| 62 | 
            +
                    else
         | 
| 63 | 
            +
                      # Fallback to basic Dockerfile if template not found
         | 
| 64 | 
            +
                      generate_basic_dockerfile
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    file_path = File.join(@devcontainer_dir, "Dockerfile")
         | 
| 68 | 
            +
                    File.write(file_path, content)
         | 
| 69 | 
            +
                    file_path
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def generate_devcontainer_json(_analysis, preferences)
         | 
| 73 | 
            +
                    # Use minimal, universal configuration
         | 
| 74 | 
            +
                    # Users can customize extensions/settings based on their project needs
         | 
| 75 | 
            +
                    config = {
         | 
| 76 | 
            +
                      name: "#{File.basename(@project_dir)} Development Container",
         | 
| 77 | 
            +
                      build: {
         | 
| 78 | 
            +
                        dockerfile: "Dockerfile",
         | 
| 79 | 
            +
                        args: {
         | 
| 80 | 
            +
                          TZ: preferences[:timezone] || "UTC"
         | 
| 81 | 
            +
                        }
         | 
| 82 | 
            +
                      },
         | 
| 83 | 
            +
                      capAdd: ["NET_ADMIN", "NET_RAW"],
         | 
| 84 | 
            +
                      mounts: [
         | 
| 85 | 
            +
                        "source=#{File.basename(@project_dir)}-bashhistory,target=/home/aidp/.bash_history,type=volume",
         | 
| 86 | 
            +
                        "source=#{File.basename(@project_dir)}-aidp,target=/home/aidp/.aidp,type=volume"
         | 
| 87 | 
            +
                      ],
         | 
| 88 | 
            +
                      postStartCommand: "sudo /usr/local/bin/init-firewall.sh",
         | 
| 89 | 
            +
                      remoteUser: "aidp",
         | 
| 90 | 
            +
                      customizations: {
         | 
| 91 | 
            +
                        vscode: {
         | 
| 92 | 
            +
                          # Include only universal, language-agnostic extensions
         | 
| 93 | 
            +
                          # Project-specific extensions should be added by the user
         | 
| 94 | 
            +
                          extensions: [
         | 
| 95 | 
            +
                            "editorconfig.editorconfig", # Respect .editorconfig
         | 
| 96 | 
            +
                            "eamodio.gitlens"            # Git integration
         | 
| 97 | 
            +
                          ]
         | 
| 98 | 
            +
                        }
         | 
| 99 | 
            +
                      }
         | 
| 100 | 
            +
                    }
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    file_path = File.join(@devcontainer_dir, "devcontainer.json")
         | 
| 103 | 
            +
                    File.write(file_path, JSON.pretty_generate(config))
         | 
| 104 | 
            +
                    file_path
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                  def generate_firewall_script(_analysis, _preferences)
         | 
| 108 | 
            +
                    # Copy template from AIDP repo
         | 
| 109 | 
            +
                    template_path = File.join(File.dirname(__FILE__), "..", "..", "..", ".devcontainer", "init-firewall.sh")
         | 
| 110 | 
            +
                    file_path = File.join(@devcontainer_dir, "init-firewall.sh")
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                    if File.exist?(template_path)
         | 
| 113 | 
            +
                      FileUtils.cp(template_path, file_path)
         | 
| 114 | 
            +
                    else
         | 
| 115 | 
            +
                      # Generate basic firewall script
         | 
| 116 | 
            +
                      content = generate_basic_firewall_script
         | 
| 117 | 
            +
                      File.write(file_path, content)
         | 
| 118 | 
            +
                    end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                    # Make executable
         | 
| 121 | 
            +
                    FileUtils.chmod(0o755, file_path)
         | 
| 122 | 
            +
                    file_path
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                  def generate_readme(_analysis, _preferences)
         | 
| 126 | 
            +
                    # Copy template from AIDP repo
         | 
| 127 | 
            +
                    template_path = File.join(File.dirname(__FILE__), "..", "..", "..", ".devcontainer", "README.md")
         | 
| 128 | 
            +
                    file_path = File.join(@devcontainer_dir, "README.md")
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                    if File.exist?(template_path)
         | 
| 131 | 
            +
                      content = File.read(template_path)
         | 
| 132 | 
            +
                      # Customize project name
         | 
| 133 | 
            +
                      content.gsub!("AIDP Development Container", "#{File.basename(@project_dir)} Development Container")
         | 
| 134 | 
            +
                      content.gsub!("AIDP", File.basename(@project_dir))
         | 
| 135 | 
            +
                    else
         | 
| 136 | 
            +
                      # Generate basic README
         | 
| 137 | 
            +
                      content = generate_basic_readme
         | 
| 138 | 
            +
                    end
         | 
| 139 | 
            +
                    File.write(file_path, content)
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                    file_path
         | 
| 142 | 
            +
                  end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                  # Generate basic Dockerfile as fallback when template not found
         | 
| 145 | 
            +
                  # Uses Ubuntu base with minimal tooling - users customize for their needs
         | 
| 146 | 
            +
                  def generate_basic_dockerfile
         | 
| 147 | 
            +
                    <<~DOCKERFILE
         | 
| 148 | 
            +
                      FROM ubuntu:22.04
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                      # Install essential system dependencies
         | 
| 151 | 
            +
                      RUN apt-get update && apt-get install -y \\
         | 
| 152 | 
            +
                          git \\
         | 
| 153 | 
            +
                          curl \\
         | 
| 154 | 
            +
                          wget \\
         | 
| 155 | 
            +
                          build-essential \\
         | 
| 156 | 
            +
                          iptables \\
         | 
| 157 | 
            +
                          ipset \\
         | 
| 158 | 
            +
                          && rm -rf /var/lib/apt/lists/*
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                      # Create non-root user
         | 
| 161 | 
            +
                      ARG USERNAME=aidp
         | 
| 162 | 
            +
                      ARG USER_UID=1000
         | 
| 163 | 
            +
                      ARG USER_GID=$USER_UID
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                      RUN groupadd --gid $USER_GID $USERNAME \\
         | 
| 166 | 
            +
                          && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \\
         | 
| 167 | 
            +
                          && echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \\
         | 
| 168 | 
            +
                          && chmod 0440 /etc/sudoers.d/$USERNAME
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                      # Set up workspace
         | 
| 171 | 
            +
                      RUN mkdir -p /workspace && chown -R $USERNAME:$USERNAME /workspace
         | 
| 172 | 
            +
                      RUN mkdir -p /home/$USERNAME/.aidp && chown -R $USERNAME:$USERNAME /home/$USERNAME/.aidp
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                      WORKDIR /workspace
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                      USER $USERNAME
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                      # Users should customize this Dockerfile for their language/framework needs
         | 
| 179 | 
            +
                      # See https://containers.dev for examples
         | 
| 180 | 
            +
                    DOCKERFILE
         | 
| 181 | 
            +
                  end
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                  def generate_basic_firewall_script
         | 
| 184 | 
            +
                    <<~BASH
         | 
| 185 | 
            +
                      #!/bin/bash
         | 
| 186 | 
            +
                      # Basic firewall configuration for devcontainer
         | 
| 187 | 
            +
                      # Allows only essential services
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                      set -e
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                      echo "Initializing firewall..."
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                      # Create ipset for allowed domains
         | 
| 194 | 
            +
                      ipset create allowed-domains hash:net -exist
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                      # DNS lookupand add common domains
         | 
| 197 | 
            +
                      add_domain() {
         | 
| 198 | 
            +
                          local domain=$1
         | 
| 199 | 
            +
                          local ips=$(dig +short "$domain" A)
         | 
| 200 | 
            +
                          for ip in $ips; do
         | 
| 201 | 
            +
                              if [[ $ip =~ ^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$ ]]; then
         | 
| 202 | 
            +
                                  ipset add allowed-domains "$ip" -exist
         | 
| 203 | 
            +
                              fi
         | 
| 204 | 
            +
                          done
         | 
| 205 | 
            +
                      }
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                      # Allow essential services
         | 
| 208 | 
            +
                      add_domain "github.com"
         | 
| 209 | 
            +
                      add_domain "api.github.com"
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                      # Set default policies
         | 
| 212 | 
            +
                      iptables -P INPUT DROP
         | 
| 213 | 
            +
                      iptables -P FORWARD DROP
         | 
| 214 | 
            +
                      iptables -P OUTPUT DROP
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                      # Allow loopback
         | 
| 217 | 
            +
                      iptables -A INPUT -i lo -j ACCEPT
         | 
| 218 | 
            +
                      iptables -A OUTPUT -o lo -j ACCEPT
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                      # Allow established connections
         | 
| 221 | 
            +
                      iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
         | 
| 222 | 
            +
                      iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                      # Allow DNS
         | 
| 225 | 
            +
                      iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
         | 
| 226 | 
            +
                      iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
         | 
| 227 | 
            +
             | 
| 228 | 
            +
                      # Allow SSH
         | 
| 229 | 
            +
                      iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
         | 
| 230 | 
            +
             | 
| 231 | 
            +
                      # Allow HTTPS to allowed domains
         | 
| 232 | 
            +
                      iptables -A OUTPUT -p tcp --dport 443 -m set --match-set allowed-domains dst -j ACCEPT
         | 
| 233 | 
            +
             | 
| 234 | 
            +
                      echo "Firewall initialized successfully"
         | 
| 235 | 
            +
                    BASH
         | 
| 236 | 
            +
                  end
         | 
| 237 | 
            +
             | 
| 238 | 
            +
                  def generate_basic_readme
         | 
| 239 | 
            +
                    <<~README
         | 
| 240 | 
            +
                      # #{File.basename(@project_dir)} Development Container
         | 
| 241 | 
            +
             | 
| 242 | 
            +
                      This directory contains the development container configuration for this project.
         | 
| 243 | 
            +
             | 
| 244 | 
            +
                      ## Prerequisites
         | 
| 245 | 
            +
             | 
| 246 | 
            +
                      - [VS Code](https://code.visualstudio.com/)
         | 
| 247 | 
            +
                      - [Docker Desktop](https://www.docker.com/products/docker-desktop/)
         | 
| 248 | 
            +
                      - [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
         | 
| 249 | 
            +
             | 
| 250 | 
            +
                      ## Usage
         | 
| 251 | 
            +
             | 
| 252 | 
            +
                      1. Open this project in VS Code
         | 
| 253 | 
            +
                      2. Press `F1` and select "Dev Containers: Reopen in Container"
         | 
| 254 | 
            +
                      3. Wait for the container to build
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                      ## Features
         | 
| 257 | 
            +
             | 
| 258 | 
            +
                      - Sandboxed development environment
         | 
| 259 | 
            +
                      - Network security with firewall
         | 
| 260 | 
            +
                      - All development tools pre-installed
         | 
| 261 | 
            +
                      - Persistent volumes for history and configuration
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                      ## Customization
         | 
| 264 | 
            +
             | 
| 265 | 
            +
                      Edit the files in `.devcontainer/` to customize the environment:
         | 
| 266 | 
            +
             | 
| 267 | 
            +
                      - `Dockerfile` - Base image and system packages
         | 
| 268 | 
            +
                      - `devcontainer.json` - VS Code settings and extensions
         | 
| 269 | 
            +
                      - `init-firewall.sh` - Network security rules
         | 
| 270 | 
            +
                    README
         | 
| 271 | 
            +
                  end
         | 
| 272 | 
            +
                end
         | 
| 273 | 
            +
              end
         | 
| 274 | 
            +
            end
         |