aidp 0.19.1 → 0.20.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/lib/aidp/cli.rb +12 -0
 - data/lib/aidp/harness/state/persistence.rb +36 -19
 - data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -1
 - data/lib/aidp/logger.rb +13 -1
 - data/lib/aidp/rescue_logging.rb +20 -9
 - data/lib/aidp/setup/wizard.rb +277 -56
 - data/lib/aidp/version.rb +1 -1
 - data/lib/aidp/workflows/guided_agent.rb +98 -13
 - data/lib/aidp.rb +1 -0
 - metadata +1 -1
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 724d07296380ad69a72b1f9d6bf4dd44d77cf35f994eb35e1e0d224ed32e93a0
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: f54213c88e3e532205852f8dfbedcc0c264a446a65a1110ddf7de524f10e64d5
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: f8788f4f8af45642b276e350f7d64e2413ae1532cbe02fe29c95c06c8fcbf84ecd66c35cb73e5b16637b6780f52ba77991b255d78f5f2464200a5db6acce262b
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 2cd95f1e5369205332ad1f87d313b6dc99801d7bdb6f13d0b661cfbba4a771a9e8277e6e8f43e8234a2bea1a6ba23cc73ef54e480f2cf73788bc6dd17aa8de74
         
     | 
    
        data/lib/aidp/cli.rb
    CHANGED
    
    | 
         @@ -146,6 +146,16 @@ module Aidp 
     | 
|
| 
       146 
146 
     | 
    
         | 
| 
       147 
147 
     | 
    
         
             
                class << self
         
     | 
| 
       148 
148 
     | 
    
         
             
                  extend Aidp::MessageDisplay::ClassMethods
         
     | 
| 
      
 149 
     | 
    
         
            +
                  extend Aidp::RescueLogging
         
     | 
| 
      
 150 
     | 
    
         
            +
             
     | 
| 
      
 151 
     | 
    
         
            +
                  # Explicit singleton delegator (defensive: ensure availability even if extend fails to attach)
         
     | 
| 
      
 152 
     | 
    
         
            +
                  def log_rescue(error, component:, action:, fallback: nil, level: :warn, **context)
         
     | 
| 
      
 153 
     | 
    
         
            +
                    Aidp::RescueLogging.log_rescue(error, component: component, action: action, fallback: fallback, level: level, **context)
         
     | 
| 
      
 154 
     | 
    
         
            +
                  end
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
                  # Store last parsed options for access by UI components (e.g., verbose flag)
         
     | 
| 
      
 157 
     | 
    
         
            +
                  @last_options = nil
         
     | 
| 
      
 158 
     | 
    
         
            +
                  attr_accessor :last_options
         
     | 
| 
       149 
159 
     | 
    
         | 
| 
       150 
160 
     | 
    
         
             
                  def create_prompt
         
     | 
| 
       151 
161 
     | 
    
         
             
                    ::TTY::Prompt.new
         
     | 
| 
         @@ -156,6 +166,7 @@ module Aidp 
     | 
|
| 
       156 
166 
     | 
    
         
             
                    return run_subcommand(args) if subcommand?(args)
         
     | 
| 
       157 
167 
     | 
    
         | 
| 
       158 
168 
     | 
    
         
             
                    options = parse_options(args)
         
     | 
| 
      
 169 
     | 
    
         
            +
                    self.last_options = options
         
     | 
| 
       159 
170 
     | 
    
         | 
| 
       160 
171 
     | 
    
         
             
                    if options[:help]
         
     | 
| 
       161 
172 
     | 
    
         
             
                      display_message(options[:parser].to_s, type: :info)
         
     | 
| 
         @@ -324,6 +335,7 @@ module Aidp 
     | 
|
| 
       324 
335 
     | 
    
         
             
                      opts.on("-h", "--help", "Show this help message") { options[:help] = true }
         
     | 
| 
       325 
336 
     | 
    
         
             
                      opts.on("-v", "--version", "Show version information") { options[:version] = true }
         
     | 
| 
       326 
337 
     | 
    
         
             
                      opts.on("--setup-config", "Setup or reconfigure config file") { options[:setup_config] = true }
         
     | 
| 
      
 338 
     | 
    
         
            +
                      opts.on("--verbose", "Show detailed prompts and raw provider responses during guided workflow") { options[:verbose] = true }
         
     | 
| 
       327 
339 
     | 
    
         | 
| 
       328 
340 
     | 
    
         
             
                      opts.separator ""
         
     | 
| 
       329 
341 
     | 
    
         
             
                      opts.separator "Examples:"
         
     | 
| 
         @@ -18,21 +18,27 @@ module Aidp 
     | 
|
| 
       18 
18 
     | 
    
         
             
                      # Callers should set skip_persistence: true for test/dry-run scenarios
         
     | 
| 
       19 
19 
     | 
    
         
             
                      @skip_persistence = skip_persistence
         
     | 
| 
       20 
20 
     | 
    
         
             
                      ensure_state_directory
         
     | 
| 
      
 21 
     | 
    
         
            +
                      Aidp.log_debug("state_persistence", "initialized", mode: @mode, skip: @skip_persistence, dir: @state_dir)
         
     | 
| 
       21 
22 
     | 
    
         
             
                    end
         
     | 
| 
       22 
23 
     | 
    
         | 
| 
       23 
24 
     | 
    
         
             
                    def has_state?
         
     | 
| 
       24 
25 
     | 
    
         
             
                      return false if @skip_persistence
         
     | 
| 
       25 
     | 
    
         
            -
                      File.exist?(@state_file)
         
     | 
| 
      
 26 
     | 
    
         
            +
                      exists = File.exist?(@state_file)
         
     | 
| 
      
 27 
     | 
    
         
            +
                      Aidp.log_debug("state_persistence", "has_state?", exists: exists, file: @state_file) if exists
         
     | 
| 
      
 28 
     | 
    
         
            +
                      exists
         
     | 
| 
       26 
29 
     | 
    
         
             
                    end
         
     | 
| 
       27 
30 
     | 
    
         | 
| 
       28 
31 
     | 
    
         
             
                    def load_state
         
     | 
| 
       29 
32 
     | 
    
         
             
                      return {} if @skip_persistence || !has_state?
         
     | 
| 
       30 
33 
     | 
    
         | 
| 
       31 
34 
     | 
    
         
             
                      with_lock do
         
     | 
| 
      
 35 
     | 
    
         
            +
                        Aidp.log_debug("state_persistence", "load_state.start", file: @state_file)
         
     | 
| 
       32 
36 
     | 
    
         
             
                        content = File.read(@state_file)
         
     | 
| 
       33 
     | 
    
         
            -
                        JSON.parse(content, symbolize_names: true)
         
     | 
| 
      
 37 
     | 
    
         
            +
                        parsed = JSON.parse(content, symbolize_names: true)
         
     | 
| 
      
 38 
     | 
    
         
            +
                        Aidp.log_debug("state_persistence", "load_state.success", keys: parsed.keys.size, file: @state_file)
         
     | 
| 
      
 39 
     | 
    
         
            +
                        parsed
         
     | 
| 
       34 
40 
     | 
    
         
             
                      rescue JSON::ParserError => e
         
     | 
| 
       35 
     | 
    
         
            -
                         
     | 
| 
      
 41 
     | 
    
         
            +
                        Aidp.log_warn("state_persistence", "parse_error", error: e.message, file: @state_file)
         
     | 
| 
       36 
42 
     | 
    
         
             
                        {}
         
     | 
| 
       37 
43 
     | 
    
         
             
                      end
         
     | 
| 
       38 
44 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -41,8 +47,10 @@ module Aidp 
     | 
|
| 
       41 
47 
     | 
    
         
             
                      return if @skip_persistence
         
     | 
| 
       42 
48 
     | 
    
         | 
| 
       43 
49 
     | 
    
         
             
                      with_lock do
         
     | 
| 
      
 50 
     | 
    
         
            +
                        Aidp.log_debug("state_persistence", "save_state.start", keys: state_data.keys.size)
         
     | 
| 
       44 
51 
     | 
    
         
             
                        state_with_metadata = add_metadata(state_data)
         
     | 
| 
       45 
52 
     | 
    
         
             
                        write_atomically(state_with_metadata)
         
     | 
| 
      
 53 
     | 
    
         
            +
                        Aidp.log_debug("state_persistence", "save_state.written", file: @state_file, size: state_with_metadata.keys.size)
         
     | 
| 
       46 
54 
     | 
    
         
             
                      end
         
     | 
| 
       47 
55 
     | 
    
         
             
                    end
         
     | 
| 
       48 
56 
     | 
    
         | 
| 
         @@ -50,7 +58,9 @@ module Aidp 
     | 
|
| 
       50 
58 
     | 
    
         
             
                      return if @skip_persistence
         
     | 
| 
       51 
59 
     | 
    
         | 
| 
       52 
60 
     | 
    
         
             
                      with_lock do
         
     | 
| 
      
 61 
     | 
    
         
            +
                        Aidp.log_debug("state_persistence", "clear_state.start", file: @state_file)
         
     | 
| 
       53 
62 
     | 
    
         
             
                        File.delete(@state_file) if File.exist?(@state_file)
         
     | 
| 
      
 63 
     | 
    
         
            +
                        Aidp.log_debug("state_persistence", "clear_state.done", file: @state_file)
         
     | 
| 
       54 
64 
     | 
    
         
             
                      end
         
     | 
| 
       55 
65 
     | 
    
         
             
                    end
         
     | 
| 
       56 
66 
     | 
    
         | 
| 
         @@ -66,8 +76,10 @@ module Aidp 
     | 
|
| 
       66 
76 
     | 
    
         | 
| 
       67 
77 
     | 
    
         
             
                    def write_atomically(state_with_metadata)
         
     | 
| 
       68 
78 
     | 
    
         
             
                      temp_file = "#{@state_file}.tmp"
         
     | 
| 
      
 79 
     | 
    
         
            +
                      Aidp.log_debug("state_persistence", "write_atomically.start", temp: temp_file)
         
     | 
| 
       69 
80 
     | 
    
         
             
                      File.write(temp_file, JSON.pretty_generate(state_with_metadata))
         
     | 
| 
       70 
81 
     | 
    
         
             
                      File.rename(temp_file, @state_file)
         
     | 
| 
      
 82 
     | 
    
         
            +
                      Aidp.log_debug("state_persistence", "write_atomically.rename", file: @state_file)
         
     | 
| 
       71 
83 
     | 
    
         
             
                    end
         
     | 
| 
       72 
84 
     | 
    
         | 
| 
       73 
85 
     | 
    
         
             
                    def ensure_state_directory
         
     | 
| 
         @@ -76,45 +88,50 @@ module Aidp 
     | 
|
| 
       76 
88 
     | 
    
         | 
| 
       77 
89 
     | 
    
         
             
                    def with_lock(&block)
         
     | 
| 
       78 
90 
     | 
    
         
             
                      return yield if @skip_persistence
         
     | 
| 
       79 
     | 
    
         
            -
             
     | 
| 
       80 
     | 
    
         
            -
                       
     | 
| 
      
 91 
     | 
    
         
            +
                      result = acquire_lock_with_timeout(&block)
         
     | 
| 
      
 92 
     | 
    
         
            +
                      result
         
     | 
| 
       81 
93 
     | 
    
         
             
                    ensure
         
     | 
| 
       82 
94 
     | 
    
         
             
                      cleanup_lock_file
         
     | 
| 
       83 
95 
     | 
    
         
             
                    end
         
     | 
| 
       84 
96 
     | 
    
         | 
| 
       85 
97 
     | 
    
         
             
                    def acquire_lock_with_timeout(&block)
         
     | 
| 
       86 
     | 
    
         
            -
                       
     | 
| 
       87 
     | 
    
         
            -
                      timeout = 30
         
     | 
| 
      
 98 
     | 
    
         
            +
                      timeout = ENV["AIDP_STATE_LOCK_TIMEOUT"]&.to_f || ((ENV["RSPEC_RUNNING"] == "true") ? 1.0 : 30.0)
         
     | 
| 
       88 
99 
     | 
    
         
             
                      start_time = Time.now
         
     | 
| 
       89 
     | 
    
         
            -
             
     | 
| 
      
 100 
     | 
    
         
            +
                      attempt_result = nil
         
     | 
| 
       90 
101 
     | 
    
         
             
                      while (Time.now - start_time) < timeout
         
     | 
| 
       91 
     | 
    
         
            -
                         
     | 
| 
       92 
     | 
    
         
            -
                         
     | 
| 
      
 102 
     | 
    
         
            +
                        acquired, attempt_result = try_acquire_lock(&block)
         
     | 
| 
      
 103 
     | 
    
         
            +
                        return attempt_result if acquired
         
     | 
| 
       93 
104 
     | 
    
         
             
                        sleep_briefly
         
     | 
| 
       94 
105 
     | 
    
         
             
                      end
         
     | 
| 
       95 
     | 
    
         
            -
             
     | 
| 
       96 
     | 
    
         
            -
                      raise_lock_timeout_error unless lock_acquired
         
     | 
| 
      
 106 
     | 
    
         
            +
                      raise_lock_timeout_error(timeout)
         
     | 
| 
       97 
107 
     | 
    
         
             
                    end
         
     | 
| 
       98 
108 
     | 
    
         | 
| 
       99 
109 
     | 
    
         
             
                    def try_acquire_lock(&block)
         
     | 
| 
       100 
110 
     | 
    
         
             
                      File.open(@lock_file, File::CREAT | File::EXCL | File::WRONLY) do |_lock|
         
     | 
| 
       101 
     | 
    
         
            -
                         
     | 
| 
       102 
     | 
    
         
            -
                        true
         
     | 
| 
      
 111 
     | 
    
         
            +
                        Aidp.log_debug("state_persistence", "lock.acquired", file: @lock_file)
         
     | 
| 
      
 112 
     | 
    
         
            +
                        [true, yield]
         
     | 
| 
       103 
113 
     | 
    
         
             
                      end
         
     | 
| 
       104 
114 
     | 
    
         
             
                    rescue Errno::EEXIST
         
     | 
| 
       105 
     | 
    
         
            -
                       
     | 
| 
      
 115 
     | 
    
         
            +
                      Aidp.log_debug("state_persistence", "lock.busy", file: @lock_file)
         
     | 
| 
      
 116 
     | 
    
         
            +
                      [false, nil]
         
     | 
| 
       106 
117 
     | 
    
         
             
                    end
         
     | 
| 
       107 
118 
     | 
    
         | 
| 
       108 
119 
     | 
    
         
             
                    def sleep_briefly
         
     | 
| 
       109 
     | 
    
         
            -
                      sleep(0. 
     | 
| 
      
 120 
     | 
    
         
            +
                      sleep(ENV["AIDP_STATE_LOCK_SLEEP"]&.to_f || 0.05)
         
     | 
| 
       110 
121 
     | 
    
         
             
                    end
         
     | 
| 
       111 
122 
     | 
    
         | 
| 
       112 
     | 
    
         
            -
                    def raise_lock_timeout_error
         
     | 
| 
       113 
     | 
    
         
            -
                       
     | 
| 
      
 123 
     | 
    
         
            +
                    def raise_lock_timeout_error(timeout)
         
     | 
| 
      
 124 
     | 
    
         
            +
                      # Prefer explicit error class; fall back if not defined yet
         
     | 
| 
      
 125 
     | 
    
         
            +
                      error_class = defined?(Aidp::Errors::StateError) ? Aidp::Errors::StateError : RuntimeError
         
     | 
| 
      
 126 
     | 
    
         
            +
                      Aidp.log_error("state_persistence", "lock.timeout", file: @lock_file, waited: timeout)
         
     | 
| 
      
 127 
     | 
    
         
            +
                      raise error_class, "Could not acquire state lock within #{timeout} seconds"
         
     | 
| 
       114 
128 
     | 
    
         
             
                    end
         
     | 
| 
       115 
129 
     | 
    
         | 
| 
       116 
130 
     | 
    
         
             
                    def cleanup_lock_file
         
     | 
| 
       117 
     | 
    
         
            -
                       
     | 
| 
      
 131 
     | 
    
         
            +
                      if File.exist?(@lock_file)
         
     | 
| 
      
 132 
     | 
    
         
            +
                        File.delete(@lock_file)
         
     | 
| 
      
 133 
     | 
    
         
            +
                        Aidp.log_debug("state_persistence", "lock.cleaned", file: @lock_file)
         
     | 
| 
      
 134 
     | 
    
         
            +
                      end
         
     | 
| 
       118 
135 
     | 
    
         
             
                    end
         
     | 
| 
       119 
136 
     | 
    
         
             
                  end
         
     | 
| 
       120 
137 
     | 
    
         
             
                end
         
     | 
| 
         @@ -251,7 +251,10 @@ module Aidp 
     | 
|
| 
       251 
251 
     | 
    
         
             
                    def select_guided_workflow
         
     | 
| 
       252 
252 
     | 
    
         
             
                      # Use the guided agent to help user select workflow
         
     | 
| 
       253 
253 
     | 
    
         
             
                      # Don't pass prompt so it uses EnhancedInput with full readline support
         
     | 
| 
       254 
     | 
    
         
            -
                       
     | 
| 
      
 254 
     | 
    
         
            +
                      verbose_flag = (defined?(Aidp::CLI) && Aidp::CLI.respond_to?(:last_options) && Aidp::CLI.last_options) ? Aidp::CLI.last_options[:verbose] : false
         
     | 
| 
      
 255 
     | 
    
         
            +
                      # Fallback: store verbose in an env for easier access if options not available
         
     | 
| 
      
 256 
     | 
    
         
            +
                      verbose = verbose_flag || ENV["AIDP_VERBOSE"] == "1"
         
     | 
| 
      
 257 
     | 
    
         
            +
                      guided_agent = Aidp::Workflows::GuidedAgent.new(@project_dir, verbose: verbose)
         
     | 
| 
       255 
258 
     | 
    
         
             
                      result = guided_agent.select_workflow
         
     | 
| 
       256 
259 
     | 
    
         | 
| 
       257 
260 
     | 
    
         
             
                      # Store user input for later use
         
     | 
    
        data/lib/aidp/logger.rb
    CHANGED
    
    | 
         @@ -33,7 +33,7 @@ module Aidp 
     | 
|
| 
       33 
33 
     | 
    
         
             
                attr_reader :level, :json_format
         
     | 
| 
       34 
34 
     | 
    
         | 
| 
       35 
35 
     | 
    
         
             
                def initialize(project_dir = Dir.pwd, config = {})
         
     | 
| 
       36 
     | 
    
         
            -
                  @project_dir = project_dir
         
     | 
| 
      
 36 
     | 
    
         
            +
                  @project_dir = sanitize_project_dir(project_dir)
         
     | 
| 
       37 
37 
     | 
    
         
             
                  @config = config
         
     | 
| 
       38 
38 
     | 
    
         
             
                  @level = determine_log_level
         
     | 
| 
       39 
39 
     | 
    
         
             
                  @json_format = config[:json] || false
         
     | 
| 
         @@ -195,6 +195,18 @@ module Aidp 
     | 
|
| 
       195 
195 
     | 
    
         
             
                def redact_hash(hash)
         
     | 
| 
       196 
196 
     | 
    
         
             
                  hash.transform_values { |v| v.is_a?(String) ? redact(v) : v }
         
     | 
| 
       197 
197 
     | 
    
         
             
                end
         
     | 
| 
      
 198 
     | 
    
         
            +
             
     | 
| 
      
 199 
     | 
    
         
            +
                # Guard against accidentally passing stream sentinel strings or invalid characters
         
     | 
| 
      
 200 
     | 
    
         
            +
                # that would create odd top-level directories like "<STDERR>".
         
     | 
| 
      
 201 
     | 
    
         
            +
                def sanitize_project_dir(dir)
         
     | 
| 
      
 202 
     | 
    
         
            +
                  return Dir.pwd if dir.nil?
         
     | 
| 
      
 203 
     | 
    
         
            +
                  str = dir.to_s
         
     | 
| 
      
 204 
     | 
    
         
            +
                  if str.empty? || str.match?(/[<>|]/) || str.match?(/[\x00-\x1F]/)
         
     | 
| 
      
 205 
     | 
    
         
            +
                    Kernel.warn "[AIDP Logger] Invalid project_dir '#{str}' - falling back to #{Dir.pwd}"
         
     | 
| 
      
 206 
     | 
    
         
            +
                    return Dir.pwd
         
     | 
| 
      
 207 
     | 
    
         
            +
                  end
         
     | 
| 
      
 208 
     | 
    
         
            +
                  str
         
     | 
| 
      
 209 
     | 
    
         
            +
                end
         
     | 
| 
       198 
210 
     | 
    
         
             
              end
         
     | 
| 
       199 
211 
     | 
    
         | 
| 
       200 
212 
     | 
    
         
             
              # Module-level logger accessor
         
     | 
    
        data/lib/aidp/rescue_logging.rb
    CHANGED
    
    | 
         @@ -13,24 +13,35 @@ module Aidp 
     | 
|
| 
       13 
13 
     | 
    
         
             
              #   - includes error class, message
         
     | 
| 
       14 
14 
     | 
    
         
             
              #   - optional fallback and extra context hash merged in
         
     | 
| 
       15 
15 
     | 
    
         
             
              module RescueLogging
         
     | 
| 
      
 16 
     | 
    
         
            +
                # Instance-level helper (made public so extend works for singleton contexts)
         
     | 
| 
       16 
17 
     | 
    
         
             
                def log_rescue(error, component:, action:, fallback: nil, level: :warn, **context)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  Aidp::RescueLogging.__log_rescue_impl(self, error, component: component, action: action, fallback: fallback, level: level, **context)
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                # Module-level access (Aidp::RescueLogging.log_rescue) for direct calls if desired
         
     | 
| 
      
 22 
     | 
    
         
            +
                def self.log_rescue(error, component:, action:, fallback: nil, level: :warn, **context)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  Aidp::RescueLogging.__log_rescue_impl(self, error, component: component, action: action, fallback: fallback, level: level, **context)
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                # Internal implementation shared by instance & module forms
         
     | 
| 
      
 27 
     | 
    
         
            +
                def self.__log_rescue_impl(context_object, error, component:, action:, fallback:, level:, **extra)
         
     | 
| 
       17 
28 
     | 
    
         
             
                  data = {
         
     | 
| 
       18 
29 
     | 
    
         
             
                    error_class: error.class.name,
         
     | 
| 
       19 
30 
     | 
    
         
             
                    error_message: error.message,
         
     | 
| 
       20 
31 
     | 
    
         
             
                    action: action
         
     | 
| 
       21 
32 
     | 
    
         
             
                  }
         
     | 
| 
       22 
33 
     | 
    
         
             
                  data[:fallback] = fallback if fallback
         
     | 
| 
       23 
     | 
    
         
            -
                  data.merge!( 
     | 
| 
      
 34 
     | 
    
         
            +
                  data.merge!(extra) unless extra.empty?
         
     | 
| 
       24 
35 
     | 
    
         | 
| 
       25 
     | 
    
         
            -
                   
     | 
| 
       26 
     | 
    
         
            -
             
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
      
 36 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 37 
     | 
    
         
            +
                    if context_object.respond_to?(:debug_log)
         
     | 
| 
      
 38 
     | 
    
         
            +
                      context_object.debug_log("⚠️ Rescue in #{component}: #{action}", level: level, data: data)
         
     | 
| 
      
 39 
     | 
    
         
            +
                    else
         
     | 
| 
      
 40 
     | 
    
         
            +
                      Aidp.logger.send(level, component, "Rescued exception during #{action}", **data)
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
      
 42 
     | 
    
         
            +
                  rescue => logging_error
         
     | 
| 
      
 43 
     | 
    
         
            +
                    warn "[AIDP Rescue Logging Error] Failed to log rescue for #{component}:#{action} - #{error.class}: #{error.message} (logging error: #{logging_error.message})"
         
     | 
| 
       30 
44 
     | 
    
         
             
                  end
         
     | 
| 
       31 
     | 
    
         
            -
                rescue => logging_error
         
     | 
| 
       32 
     | 
    
         
            -
                  # Last resort: avoid raising from logging path - fall back to STDERR
         
     | 
| 
       33 
     | 
    
         
            -
                  warn "[AIDP Rescue Logging Error] Failed to log rescue for #{component}:#{action} - #{error.class}: #{error.message} (logging error: #{logging_error.message})"
         
     | 
| 
       34 
45 
     | 
    
         
             
                end
         
     | 
| 
       35 
46 
     | 
    
         
             
              end
         
     | 
| 
       36 
47 
     | 
    
         
             
            end
         
     | 
    
        data/lib/aidp/setup/wizard.rb
    CHANGED
    
    | 
         @@ -30,6 +30,8 @@ module Aidp 
     | 
|
| 
       30 
30 
     | 
    
         | 
| 
       31 
31 
     | 
    
         
             
                  def run
         
     | 
| 
       32 
32 
     | 
    
         
             
                    display_welcome
         
     | 
| 
      
 33 
     | 
    
         
            +
                    # Normalize any legacy or label-based model_family entries before prompting
         
     | 
| 
      
 34 
     | 
    
         
            +
                    normalize_existing_model_families!
         
     | 
| 
       33 
35 
     | 
    
         
             
                    return @saved if skip_wizard?
         
     | 
| 
       34 
36 
     | 
    
         | 
| 
       35 
37 
     | 
    
         
             
                    configure_providers
         
     | 
| 
         @@ -150,11 +152,30 @@ module Aidp 
     | 
|
| 
       150 
152 
     | 
    
         
             
                    fallback_choices = available_providers.reject { |_, name| name == provider_choice }
         
     | 
| 
       151 
153 
     | 
    
         
             
                    fallback_default_names = existing_fallbacks.filter_map { |provider_name| fallback_choices.key(provider_name) }
         
     | 
| 
       152 
154 
     | 
    
         | 
| 
      
 155 
     | 
    
         
            +
                    prompt.say("\n💡 Use ↑/↓ arrows to navigate, SPACE to select/deselect, ENTER to confirm")
         
     | 
| 
       153 
156 
     | 
    
         
             
                    fallback_selected = prompt.multi_select("Select fallback providers (used if primary fails):", default: fallback_default_names) do |menu|
         
     | 
| 
       154 
157 
     | 
    
         
             
                      fallback_choices.each do |display_name, provider_name|
         
     | 
| 
       155 
158 
     | 
    
         
             
                        menu.choice display_name, provider_name
         
     | 
| 
       156 
159 
     | 
    
         
             
                      end
         
     | 
| 
       157 
160 
     | 
    
         
             
                    end
         
     | 
| 
      
 161 
     | 
    
         
            +
                    if ENV["AIDP_FALLBACK_DEBUG"] == "1"
         
     | 
| 
      
 162 
     | 
    
         
            +
                      prompt.say("[debug] raw multi_select fallback_selected=#{fallback_selected.inspect}")
         
     | 
| 
      
 163 
     | 
    
         
            +
                    end
         
     | 
| 
      
 164 
     | 
    
         
            +
                    # Recovery: if multi_select unexpectedly returns empty and there were no existing fallbacks, offer a single-select
         
     | 
| 
      
 165 
     | 
    
         
            +
                    if fallback_selected.empty? && existing_fallbacks.empty? && !fallback_choices.empty?
         
     | 
| 
      
 166 
     | 
    
         
            +
                      if ENV["AIDP_FALLBACK_DEBUG"] == "1"
         
     | 
| 
      
 167 
     | 
    
         
            +
                        prompt.say("[debug] invoking recovery single-select for first fallback")
         
     | 
| 
      
 168 
     | 
    
         
            +
                      end
         
     | 
| 
      
 169 
     | 
    
         
            +
                      if prompt.yes?("No fallback selected. Add one?", default: true)
         
     | 
| 
      
 170 
     | 
    
         
            +
                        recovery_choice = prompt.select("Select a fallback provider:") do |menu|
         
     | 
| 
      
 171 
     | 
    
         
            +
                          fallback_choices.each do |display_name, provider_name|
         
     | 
| 
      
 172 
     | 
    
         
            +
                            menu.choice display_name, provider_name
         
     | 
| 
      
 173 
     | 
    
         
            +
                          end
         
     | 
| 
      
 174 
     | 
    
         
            +
                          menu.choice "Skip", :skip
         
     | 
| 
      
 175 
     | 
    
         
            +
                        end
         
     | 
| 
      
 176 
     | 
    
         
            +
                        fallback_selected = [recovery_choice] unless recovery_choice == :skip
         
     | 
| 
      
 177 
     | 
    
         
            +
                      end
         
     | 
| 
      
 178 
     | 
    
         
            +
                    end
         
     | 
| 
       158 
179 
     | 
    
         | 
| 
       159 
180 
     | 
    
         
             
                    # If user selected none but we had existing fallbacks, confirm removal
         
     | 
| 
       160 
181 
     | 
    
         
             
                    if fallback_selected.empty? && existing_fallbacks.any?
         
     | 
| 
         @@ -167,10 +188,81 @@ module Aidp 
     | 
|
| 
       167 
188 
     | 
    
         
             
                    set([:harness, :fallback_providers], cleaned_fallbacks)
         
     | 
| 
       168 
189 
     | 
    
         | 
| 
       169 
190 
     | 
    
         
             
                    # Auto-create minimal provider configs for fallbacks if missing
         
     | 
| 
       170 
     | 
    
         
            -
                    cleaned_fallbacks.each  
     | 
| 
      
 191 
     | 
    
         
            +
                    cleaned_fallbacks.each do |fp|
         
     | 
| 
      
 192 
     | 
    
         
            +
                      prompt.say("[debug] ensuring billing config for fallback '#{fp}'") if ENV["AIDP_FALLBACK_DEBUG"] == "1"
         
     | 
| 
      
 193 
     | 
    
         
            +
                      ensure_provider_billing_config(fp, force: true)
         
     | 
| 
      
 194 
     | 
    
         
            +
                    end
         
     | 
| 
      
 195 
     | 
    
         
            +
             
     | 
| 
      
 196 
     | 
    
         
            +
                    # Offer editing of existing provider configurations (primary + fallbacks)
         
     | 
| 
      
 197 
     | 
    
         
            +
                    # (editable will be recomputed after any additional fallback additions)
         
     | 
| 
      
 198 
     | 
    
         
            +
                    ([provider_choice] + cleaned_fallbacks).uniq.reject { |p| p == "custom" }
         
     | 
| 
      
 199 
     | 
    
         
            +
             
     | 
| 
      
 200 
     | 
    
         
            +
                    # Optional: allow adding more fallbacks iteratively
         
     | 
| 
      
 201 
     | 
    
         
            +
                    if prompt.yes?("Add another fallback provider?", default: false)
         
     | 
| 
      
 202 
     | 
    
         
            +
                      loop do
         
     | 
| 
      
 203 
     | 
    
         
            +
                        remaining = available_providers.reject { |_, name| ([provider_choice] + cleaned_fallbacks).include?(name) }
         
     | 
| 
      
 204 
     | 
    
         
            +
                        break if remaining.empty?
         
     | 
| 
      
 205 
     | 
    
         
            +
                        add_choice = prompt.select("Select additional fallback provider:") do |menu|
         
     | 
| 
      
 206 
     | 
    
         
            +
                          remaining.each { |display, name| menu.choice display, name }
         
     | 
| 
      
 207 
     | 
    
         
            +
                          menu.choice "Done", :done
         
     | 
| 
      
 208 
     | 
    
         
            +
                        end
         
     | 
| 
      
 209 
     | 
    
         
            +
                        break if add_choice == :done
         
     | 
| 
      
 210 
     | 
    
         
            +
                        unless cleaned_fallbacks.include?(add_choice)
         
     | 
| 
      
 211 
     | 
    
         
            +
                          cleaned_fallbacks << add_choice
         
     | 
| 
      
 212 
     | 
    
         
            +
                          set([:harness, :fallback_providers], cleaned_fallbacks)
         
     | 
| 
      
 213 
     | 
    
         
            +
                          ensure_provider_billing_config(add_choice, force: true)
         
     | 
| 
      
 214 
     | 
    
         
            +
                        end
         
     | 
| 
      
 215 
     | 
    
         
            +
                      end
         
     | 
| 
      
 216 
     | 
    
         
            +
                    end
         
     | 
| 
      
 217 
     | 
    
         
            +
                    # Recompute editable after additions
         
     | 
| 
      
 218 
     | 
    
         
            +
                    editable = ([provider_choice] + cleaned_fallbacks).uniq.reject { |p| p == "custom" }
         
     | 
| 
      
 219 
     | 
    
         
            +
                    if editable.any? && prompt.yes?("Edit provider configuration details (billing/model family)?", default: false)
         
     | 
| 
      
 220 
     | 
    
         
            +
                      loop do
         
     | 
| 
      
 221 
     | 
    
         
            +
                        # Build dynamic mapping of display names -> internal names for edit menu
         
     | 
| 
      
 222 
     | 
    
         
            +
                        available_map = discover_available_providers # {display_name => internal_name}
         
     | 
| 
      
 223 
     | 
    
         
            +
                        display_name_for = available_map.invert # {internal_name => display_name}
         
     | 
| 
      
 224 
     | 
    
         
            +
                        to_edit = prompt.select("Select a provider to edit or add:") do |menu|
         
     | 
| 
      
 225 
     | 
    
         
            +
                          editable.each do |prov|
         
     | 
| 
      
 226 
     | 
    
         
            +
                            display_label = display_name_for.fetch(prov, prov.capitalize)
         
     | 
| 
      
 227 
     | 
    
         
            +
                            menu.choice display_label, prov
         
     | 
| 
      
 228 
     | 
    
         
            +
                          end
         
     | 
| 
      
 229 
     | 
    
         
            +
                          # Sentinel option: add a new fallback provider that isn't yet in editable list
         
     | 
| 
      
 230 
     | 
    
         
            +
                          remaining = available_map.values - editable
         
     | 
| 
      
 231 
     | 
    
         
            +
                          if remaining.any?
         
     | 
| 
      
 232 
     | 
    
         
            +
                            menu.choice "➕ Add fallback provider…", :add_fallback
         
     | 
| 
      
 233 
     | 
    
         
            +
                          end
         
     | 
| 
      
 234 
     | 
    
         
            +
                          menu.choice "Done", :done
         
     | 
| 
      
 235 
     | 
    
         
            +
                        end
         
     | 
| 
      
 236 
     | 
    
         
            +
             
     | 
| 
      
 237 
     | 
    
         
            +
                        case to_edit
         
     | 
| 
      
 238 
     | 
    
         
            +
                        when :done
         
     | 
| 
      
 239 
     | 
    
         
            +
                          break
         
     | 
| 
      
 240 
     | 
    
         
            +
                        when :add_fallback
         
     | 
| 
      
 241 
     | 
    
         
            +
                          # Allow user to pick from remaining providers by display name
         
     | 
| 
      
 242 
     | 
    
         
            +
                          remaining_map = available_map.select { |disp, internal| !editable.include?(internal) && internal != provider_choice }
         
     | 
| 
      
 243 
     | 
    
         
            +
                          add_choice = prompt.select("Select provider to add as fallback:") do |menu|
         
     | 
| 
      
 244 
     | 
    
         
            +
                            remaining_map.each { |disp, internal| menu.choice disp, internal }
         
     | 
| 
      
 245 
     | 
    
         
            +
                            menu.choice "Cancel", :cancel
         
     | 
| 
      
 246 
     | 
    
         
            +
                          end
         
     | 
| 
      
 247 
     | 
    
         
            +
                          next if add_choice == :cancel
         
     | 
| 
      
 248 
     | 
    
         
            +
                          unless cleaned_fallbacks.include?(add_choice)
         
     | 
| 
      
 249 
     | 
    
         
            +
                            cleaned_fallbacks << add_choice
         
     | 
| 
      
 250 
     | 
    
         
            +
                            set([:harness, :fallback_providers], cleaned_fallbacks)
         
     | 
| 
      
 251 
     | 
    
         
            +
                            prompt.say("[debug] ensuring billing config for newly added fallback '#{add_choice}'") if ENV["AIDP_FALLBACK_DEBUG"] == "1"
         
     | 
| 
      
 252 
     | 
    
         
            +
                            ensure_provider_billing_config(add_choice, force: true)
         
     | 
| 
      
 253 
     | 
    
         
            +
                            editable = ([provider_choice] + cleaned_fallbacks).uniq.reject { |p| p == "custom" }
         
     | 
| 
      
 254 
     | 
    
         
            +
                          end
         
     | 
| 
      
 255 
     | 
    
         
            +
                        else
         
     | 
| 
      
 256 
     | 
    
         
            +
                          edit_provider_configuration(to_edit)
         
     | 
| 
      
 257 
     | 
    
         
            +
                        end
         
     | 
| 
      
 258 
     | 
    
         
            +
                      end
         
     | 
| 
      
 259 
     | 
    
         
            +
                    end
         
     | 
| 
       171 
260 
     | 
    
         | 
| 
       172 
261 
     | 
    
         
             
                    # Provide informational note (no secret handling stored)
         
     | 
| 
       173 
262 
     | 
    
         
             
                    show_provider_info_note(provider_choice) unless provider_choice == "custom"
         
     | 
| 
      
 263 
     | 
    
         
            +
             
     | 
| 
      
 264 
     | 
    
         
            +
                    # Show summary of configured providers (replaces the earlier inline summary)
         
     | 
| 
      
 265 
     | 
    
         
            +
                    show_provider_summary(provider_choice, cleaned_fallbacks) unless provider_choice == "custom"
         
     | 
| 
       174 
266 
     | 
    
         
             
                  end
         
     | 
| 
       175 
267 
     | 
    
         | 
| 
       176 
268 
     | 
    
         
             
                  # Removed MCP configuration step (MCP now expected to be provider-specific if used)
         
     | 
| 
         @@ -262,13 +354,19 @@ module Aidp 
     | 
|
| 
       262 
354 
     | 
    
         
             
                    enabled = prompt.yes?("Enable coverage tracking?", default: existing.fetch(:enabled, false))
         
     | 
| 
       263 
355 
     | 
    
         
             
                    return set([:work_loop, :coverage], {enabled: false}) unless enabled
         
     | 
| 
       264 
356 
     | 
    
         | 
| 
       265 
     | 
    
         
            -
                     
     | 
| 
       266 
     | 
    
         
            -
                       
     | 
| 
       267 
     | 
    
         
            -
                       
     | 
| 
       268 
     | 
    
         
            -
                       
     | 
| 
       269 
     | 
    
         
            -
                       
     | 
| 
       270 
     | 
    
         
            -
                       
     | 
| 
       271 
     | 
    
         
            -
                       
     | 
| 
      
 357 
     | 
    
         
            +
                    coverage_tool_choices = [
         
     | 
| 
      
 358 
     | 
    
         
            +
                      ["SimpleCov (Ruby)", "simplecov"],
         
     | 
| 
      
 359 
     | 
    
         
            +
                      ["NYC/Istanbul (JavaScript)", "nyc"],
         
     | 
| 
      
 360 
     | 
    
         
            +
                      ["Coverage.py (Python)", "coverage.py"],
         
     | 
| 
      
 361 
     | 
    
         
            +
                      ["go test -cover (Go)", "go-cover"],
         
     | 
| 
      
 362 
     | 
    
         
            +
                      ["Jest (JavaScript)", "jest"],
         
     | 
| 
      
 363 
     | 
    
         
            +
                      ["Other", "other"]
         
     | 
| 
      
 364 
     | 
    
         
            +
                    ]
         
     | 
| 
      
 365 
     | 
    
         
            +
                    coverage_tool_default = existing[:tool]
         
     | 
| 
      
 366 
     | 
    
         
            +
                    coverage_tool_default_label = coverage_tool_choices.find { |label, value| value == coverage_tool_default }&.first
         
     | 
| 
      
 367 
     | 
    
         
            +
             
     | 
| 
      
 368 
     | 
    
         
            +
                    tool = prompt.select("Which coverage tool do you use?", default: coverage_tool_default_label) do |menu|
         
     | 
| 
      
 369 
     | 
    
         
            +
                      coverage_tool_choices.each { |label, value| menu.choice label, value }
         
     | 
| 
       272 
370 
     | 
    
         
             
                    end
         
     | 
| 
       273 
371 
     | 
    
         | 
| 
       274 
372 
     | 
    
         
             
                    run_command = ask_with_default("Coverage run command", existing[:run_command] || detect_coverage_command(tool))
         
     | 
| 
         @@ -300,10 +398,16 @@ module Aidp 
     | 
|
| 
       300 
398 
     | 
    
         
             
                    enabled = prompt.yes?("Enable interactive testing tools?", default: existing.fetch(:enabled, false))
         
     | 
| 
       301 
399 
     | 
    
         
             
                    return set([:work_loop, :interactive_testing], {enabled: false}) unless enabled
         
     | 
| 
       302 
400 
     | 
    
         | 
| 
       303 
     | 
    
         
            -
                     
     | 
| 
       304 
     | 
    
         
            -
                       
     | 
| 
       305 
     | 
    
         
            -
                       
     | 
| 
       306 
     | 
    
         
            -
                       
     | 
| 
      
 401 
     | 
    
         
            +
                    app_type_choices = [
         
     | 
| 
      
 402 
     | 
    
         
            +
                      ["Web application", "web"],
         
     | 
| 
      
 403 
     | 
    
         
            +
                      ["CLI application", "cli"],
         
     | 
| 
      
 404 
     | 
    
         
            +
                      ["Desktop application", "desktop"]
         
     | 
| 
      
 405 
     | 
    
         
            +
                    ]
         
     | 
| 
      
 406 
     | 
    
         
            +
                    app_type_default = existing[:app_type]
         
     | 
| 
      
 407 
     | 
    
         
            +
                    app_type_default_label = app_type_choices.find { |label, value| value == app_type_default }&.first
         
     | 
| 
      
 408 
     | 
    
         
            +
             
     | 
| 
      
 409 
     | 
    
         
            +
                    app_type = prompt.select("What type of application are you testing?", default: app_type_default_label) do |menu|
         
     | 
| 
      
 410 
     | 
    
         
            +
                      app_type_choices.each { |label, value| menu.choice label, value }
         
     | 
| 
       307 
411 
     | 
    
         
             
                    end
         
     | 
| 
       308 
412 
     | 
    
         | 
| 
       309 
413 
     | 
    
         
             
                    tools = {}
         
     | 
| 
         @@ -382,17 +486,21 @@ module Aidp 
     | 
|
| 
       382 
486 
     | 
    
         | 
| 
       383 
487 
     | 
    
         
             
                    # Detect VCS
         
     | 
| 
       384 
488 
     | 
    
         
             
                    detected_vcs = detect_vcs_tool
         
     | 
| 
      
 489 
     | 
    
         
            +
                    vcs_choices = [
         
     | 
| 
      
 490 
     | 
    
         
            +
                      ["git", "git"],
         
     | 
| 
      
 491 
     | 
    
         
            +
                      ["svn", "svn"],
         
     | 
| 
      
 492 
     | 
    
         
            +
                      ["none (no VCS)", "none"]
         
     | 
| 
      
 493 
     | 
    
         
            +
                    ]
         
     | 
| 
      
 494 
     | 
    
         
            +
                    vcs_default = existing[:tool] || detected_vcs || "git"
         
     | 
| 
      
 495 
     | 
    
         
            +
                    vcs_default_label = vcs_choices.find { |label, value| value == vcs_default }&.first
         
     | 
| 
      
 496 
     | 
    
         
            +
             
     | 
| 
       385 
497 
     | 
    
         
             
                    vcs_tool = if detected_vcs
         
     | 
| 
       386 
     | 
    
         
            -
                      prompt.select("Detected #{detected_vcs}. Use this version control system?", default:  
     | 
| 
       387 
     | 
    
         
            -
                        menu.choice  
     | 
| 
       388 
     | 
    
         
            -
                        menu.choice "svn", "svn"
         
     | 
| 
       389 
     | 
    
         
            -
                        menu.choice "none (no VCS)", "none"
         
     | 
| 
      
 498 
     | 
    
         
            +
                      prompt.select("Detected #{detected_vcs}. Use this version control system?", default: vcs_default_label) do |menu|
         
     | 
| 
      
 499 
     | 
    
         
            +
                        vcs_choices.each { |label, value| menu.choice label, value }
         
     | 
| 
       390 
500 
     | 
    
         
             
                      end
         
     | 
| 
       391 
501 
     | 
    
         
             
                    else
         
     | 
| 
       392 
     | 
    
         
            -
                      prompt.select("Which version control system do you use?", default:  
     | 
| 
       393 
     | 
    
         
            -
                        menu.choice  
     | 
| 
       394 
     | 
    
         
            -
                        menu.choice "svn", "svn"
         
     | 
| 
       395 
     | 
    
         
            -
                        menu.choice "none (no VCS)", "none"
         
     | 
| 
      
 502 
     | 
    
         
            +
                      prompt.select("Which version control system do you use?", default: vcs_default_label) do |menu|
         
     | 
| 
      
 503 
     | 
    
         
            +
                        vcs_choices.each { |label, value| menu.choice label, value }
         
     | 
| 
       396 
504 
     | 
    
         
             
                      end
         
     | 
| 
       397 
505 
     | 
    
         
             
                    end
         
     | 
| 
       398 
506 
     | 
    
         | 
| 
         @@ -400,10 +508,18 @@ module Aidp 
     | 
|
| 
       400 
508 
     | 
    
         | 
| 
       401 
509 
     | 
    
         
             
                    prompt.say("\n📋 Commit Behavior (applies to copilot/interactive mode only)")
         
     | 
| 
       402 
510 
     | 
    
         
             
                    prompt.say("Note: Watch mode and fully automatic daemon mode will always commit changes.")
         
     | 
| 
       403 
     | 
    
         
            -
             
     | 
| 
       404 
     | 
    
         
            -
             
     | 
| 
       405 
     | 
    
         
            -
             
     | 
| 
       406 
     | 
    
         
            -
                       
     | 
| 
      
 511 
     | 
    
         
            +
             
     | 
| 
      
 512 
     | 
    
         
            +
                    # Map value defaults to choice labels for TTY::Prompt validation
         
     | 
| 
      
 513 
     | 
    
         
            +
                    behavior_choices = [
         
     | 
| 
      
 514 
     | 
    
         
            +
                      ["Do nothing (manual git operations)", "nothing"],
         
     | 
| 
      
 515 
     | 
    
         
            +
                      ["Stage changes only", "stage"],
         
     | 
| 
      
 516 
     | 
    
         
            +
                      ["Stage and commit changes", "commit"]
         
     | 
| 
      
 517 
     | 
    
         
            +
                    ]
         
     | 
| 
      
 518 
     | 
    
         
            +
                    behavior_default = existing[:behavior] || "nothing"
         
     | 
| 
      
 519 
     | 
    
         
            +
                    behavior_default_label = behavior_choices.find { |label, value| value == behavior_default }&.first
         
     | 
| 
      
 520 
     | 
    
         
            +
             
     | 
| 
      
 521 
     | 
    
         
            +
                    behavior = prompt.select("In copilot mode, should aidp:", default: behavior_default_label) do |menu|
         
     | 
| 
      
 522 
     | 
    
         
            +
                      behavior_choices.each { |label, value| menu.choice label, value }
         
     | 
| 
       407 
523 
     | 
    
         
             
                    end
         
     | 
| 
       408 
524 
     | 
    
         | 
| 
       409 
525 
     | 
    
         
             
                    # Commit message configuration
         
     | 
| 
         @@ -437,10 +553,16 @@ module Aidp 
     | 
|
| 
       437 
553 
     | 
    
         | 
| 
       438 
554 
     | 
    
         
             
                    # Commit message style
         
     | 
| 
       439 
555 
     | 
    
         
             
                    commit_style = if conventional_commits
         
     | 
| 
       440 
     | 
    
         
            -
                       
     | 
| 
       441 
     | 
    
         
            -
                         
     | 
| 
       442 
     | 
    
         
            -
                         
     | 
| 
       443 
     | 
    
         
            -
                         
     | 
| 
      
 556 
     | 
    
         
            +
                      commit_style_choices = [
         
     | 
| 
      
 557 
     | 
    
         
            +
                        ["Default (e.g., 'feat: add user authentication')", "default"],
         
     | 
| 
      
 558 
     | 
    
         
            +
                        ["Angular (with scope: 'feat(auth): add login')", "angular"],
         
     | 
| 
      
 559 
     | 
    
         
            +
                        ["Emoji (e.g., '✨ feat: add user authentication')", "emoji"]
         
     | 
| 
      
 560 
     | 
    
         
            +
                      ]
         
     | 
| 
      
 561 
     | 
    
         
            +
                      commit_style_default = existing[:commit_style] || "default"
         
     | 
| 
      
 562 
     | 
    
         
            +
                      commit_style_default_label = commit_style_choices.find { |label, value| value == commit_style_default }&.first
         
     | 
| 
      
 563 
     | 
    
         
            +
             
     | 
| 
      
 564 
     | 
    
         
            +
                      prompt.select("Conventional commit style:", default: commit_style_default_label) do |menu|
         
     | 
| 
      
 565 
     | 
    
         
            +
                        commit_style_choices.each { |label, value| menu.choice label, value }
         
     | 
| 
       444 
566 
     | 
    
         
             
                      end
         
     | 
| 
       445 
567 
     | 
    
         
             
                    else
         
     | 
| 
       446 
568 
     | 
    
         
             
                      "default"
         
     | 
| 
         @@ -476,10 +598,16 @@ module Aidp 
     | 
|
| 
       476 
598 
     | 
    
         
             
                    )
         
     | 
| 
       477 
599 
     | 
    
         | 
| 
       478 
600 
     | 
    
         
             
                    if auto_create_pr
         
     | 
| 
       479 
     | 
    
         
            -
                       
     | 
| 
       480 
     | 
    
         
            -
                         
     | 
| 
       481 
     | 
    
         
            -
                         
     | 
| 
       482 
     | 
    
         
            -
                         
     | 
| 
      
 601 
     | 
    
         
            +
                      pr_strategy_choices = [
         
     | 
| 
      
 602 
     | 
    
         
            +
                        ["Create as draft PR (safe, allows review before merge)", "draft"],
         
     | 
| 
      
 603 
     | 
    
         
            +
                        ["Create as ready PR (immediately reviewable)", "ready"],
         
     | 
| 
      
 604 
     | 
    
         
            +
                        ["Create and auto-merge (fully autonomous, requires approval rules)", "auto_merge"]
         
     | 
| 
      
 605 
     | 
    
         
            +
                      ]
         
     | 
| 
      
 606 
     | 
    
         
            +
                      pr_strategy_default = existing[:pr_strategy] || "draft"
         
     | 
| 
      
 607 
     | 
    
         
            +
                      pr_strategy_default_label = pr_strategy_choices.find { |label, value| value == pr_strategy_default }&.first
         
     | 
| 
      
 608 
     | 
    
         
            +
             
     | 
| 
      
 609 
     | 
    
         
            +
                      pr_strategy = prompt.select("PR creation strategy:", default: pr_strategy_default_label) do |menu|
         
     | 
| 
      
 610 
     | 
    
         
            +
                        pr_strategy_choices.each { |label, value| menu.choice label, value }
         
     | 
| 
       483 
611 
     | 
    
         
             
                      end
         
     | 
| 
       484 
612 
     | 
    
         | 
| 
       485 
613 
     | 
    
         
             
                      {
         
     | 
| 
         @@ -614,11 +742,16 @@ module Aidp 
     | 
|
| 
       614 
742 
     | 
    
         
             
                    prompt.say("-" * 40)
         
     | 
| 
       615 
743 
     | 
    
         
             
                    existing = get([:logging]) || {}
         
     | 
| 
       616 
744 
     | 
    
         | 
| 
       617 
     | 
    
         
            -
                     
     | 
| 
       618 
     | 
    
         
            -
             
     | 
| 
       619 
     | 
    
         
            -
                       
     | 
| 
       620 
     | 
    
         
            -
                       
     | 
| 
       621 
     | 
    
         
            -
             
     | 
| 
      
 745 
     | 
    
         
            +
                    log_level_choices = [
         
     | 
| 
      
 746 
     | 
    
         
            +
                      ["Debug", "debug"],
         
     | 
| 
      
 747 
     | 
    
         
            +
                      ["Info", "info"],
         
     | 
| 
      
 748 
     | 
    
         
            +
                      ["Error", "error"]
         
     | 
| 
      
 749 
     | 
    
         
            +
                    ]
         
     | 
| 
      
 750 
     | 
    
         
            +
                    log_level_default = existing[:level] || "info"
         
     | 
| 
      
 751 
     | 
    
         
            +
                    log_level_default_label = log_level_choices.find { |label, value| value == log_level_default }&.first
         
     | 
| 
      
 752 
     | 
    
         
            +
             
     | 
| 
      
 753 
     | 
    
         
            +
                    log_level = prompt.select("Log level:", default: log_level_default_label) do |menu|
         
     | 
| 
      
 754 
     | 
    
         
            +
                      log_level_choices.each { |label, value| menu.choice label, value }
         
     | 
| 
       622 
755 
     | 
    
         
             
                    end
         
     | 
| 
       623 
756 
     | 
    
         
             
                    json = prompt.yes?("Use JSON log format?", default: existing.fetch(:json, false))
         
     | 
| 
       624 
757 
     | 
    
         
             
                    max_size = ask_with_default("Max log size (MB)", (existing[:max_size_mb] || 10).to_s) { |value| value.to_i }
         
     | 
| 
         @@ -897,44 +1030,132 @@ module Aidp 
     | 
|
| 
       897 
1030 
     | 
    
         
             
                    prompt.say("Only the billing model (subscription vs usage_based) is recorded for fallback decisions.")
         
     | 
| 
       898 
1031 
     | 
    
         
             
                  end
         
     | 
| 
       899 
1032 
     | 
    
         | 
| 
      
 1033 
     | 
    
         
            +
                  def show_provider_summary(primary, fallbacks)
         
     | 
| 
      
 1034 
     | 
    
         
            +
                    prompt.say("\n📋 Provider Configuration Summary:")
         
     | 
| 
      
 1035 
     | 
    
         
            +
                    providers_config = get([:providers]) || {}
         
     | 
| 
      
 1036 
     | 
    
         
            +
             
     | 
| 
      
 1037 
     | 
    
         
            +
                    # Show primary
         
     | 
| 
      
 1038 
     | 
    
         
            +
                    if primary && primary != "custom"
         
     | 
| 
      
 1039 
     | 
    
         
            +
                      primary_cfg = providers_config[primary.to_sym] || {}
         
     | 
| 
      
 1040 
     | 
    
         
            +
                      prompt.say("  ✓ Primary: #{primary} (#{primary_cfg[:type] || "not configured"}, #{primary_cfg[:model_family] || "auto"})")
         
     | 
| 
      
 1041 
     | 
    
         
            +
                    end
         
     | 
| 
      
 1042 
     | 
    
         
            +
             
     | 
| 
      
 1043 
     | 
    
         
            +
                    # Show fallbacks
         
     | 
| 
      
 1044 
     | 
    
         
            +
                    if fallbacks && !fallbacks.empty?
         
     | 
| 
      
 1045 
     | 
    
         
            +
                      fallbacks.each do |fallback|
         
     | 
| 
      
 1046 
     | 
    
         
            +
                        fallback_cfg = providers_config[fallback.to_sym] || {}
         
     | 
| 
      
 1047 
     | 
    
         
            +
                        prompt.say("  ✓ Fallback: #{fallback} (#{fallback_cfg[:type] || "not configured"}, #{fallback_cfg[:model_family] || "auto"})")
         
     | 
| 
      
 1048 
     | 
    
         
            +
                      end
         
     | 
| 
      
 1049 
     | 
    
         
            +
                    end
         
     | 
| 
      
 1050 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1051 
     | 
    
         
            +
             
     | 
| 
       900 
1052 
     | 
    
         
             
                  # Ensure a minimal billing configuration exists for a selected provider (no secrets)
         
     | 
| 
       901 
     | 
    
         
            -
                  def ensure_provider_billing_config(provider_name)
         
     | 
| 
      
 1053 
     | 
    
         
            +
                  def ensure_provider_billing_config(provider_name, force: false)
         
     | 
| 
       902 
1054 
     | 
    
         
             
                    return if provider_name.nil? || provider_name == "custom"
         
     | 
| 
       903 
1055 
     | 
    
         
             
                    providers_section = get([:providers]) || {}
         
     | 
| 
       904 
1056 
     | 
    
         
             
                    existing = providers_section[provider_name.to_sym]
         
     | 
| 
       905 
1057 
     | 
    
         | 
| 
       906 
     | 
    
         
            -
                    if existing && existing[:type]
         
     | 
| 
      
 1058 
     | 
    
         
            +
                    if existing && existing[:type] && !force
         
     | 
| 
       907 
1059 
     | 
    
         
             
                      prompt.say("  • Provider '#{provider_name}' already configured (type: #{existing[:type]})")
         
     | 
| 
       908 
     | 
    
         
            -
                      # Still ask for model family if not set
         
     | 
| 
       909 
1060 
     | 
    
         
             
                      unless existing[:model_family]
         
     | 
| 
       910 
     | 
    
         
            -
                        model_family = ask_model_family(provider_name 
     | 
| 
      
 1061 
     | 
    
         
            +
                        model_family = ask_model_family(provider_name)
         
     | 
| 
       911 
1062 
     | 
    
         
             
                        set([:providers, provider_name.to_sym, :model_family], model_family)
         
     | 
| 
       912 
1063 
     | 
    
         
             
                      end
         
     | 
| 
       913 
1064 
     | 
    
         
             
                      return
         
     | 
| 
       914 
1065 
     | 
    
         
             
                    end
         
     | 
| 
       915 
1066 
     | 
    
         | 
| 
       916 
     | 
    
         
            -
                    provider_type =  
     | 
| 
       917 
     | 
    
         
            -
                    model_family = ask_model_family(provider_name)
         
     | 
| 
       918 
     | 
    
         
            -
                     
     | 
| 
       919 
     | 
    
         
            -
                     
     | 
| 
      
 1067 
     | 
    
         
            +
                    provider_type = ask_provider_billing_type_with_default(provider_name, existing&.dig(:type))
         
     | 
| 
      
 1068 
     | 
    
         
            +
                    model_family = ask_model_family(provider_name, existing&.dig(:model_family) || "auto")
         
     | 
| 
      
 1069 
     | 
    
         
            +
                    merged = (existing || {}).merge(type: provider_type, model_family: model_family)
         
     | 
| 
      
 1070 
     | 
    
         
            +
                    set([:providers, provider_name.to_sym], merged)
         
     | 
| 
      
 1071 
     | 
    
         
            +
                    normalize_existing_model_families!
         
     | 
| 
      
 1072 
     | 
    
         
            +
                    action_word = if existing
         
     | 
| 
      
 1073 
     | 
    
         
            +
                      force ? "reconfigured" : "updated"
         
     | 
| 
      
 1074 
     | 
    
         
            +
                    else
         
     | 
| 
      
 1075 
     | 
    
         
            +
                      "added"
         
     | 
| 
      
 1076 
     | 
    
         
            +
                    end
         
     | 
| 
      
 1077 
     | 
    
         
            +
                    # Enhance messaging with display name when available
         
     | 
| 
      
 1078 
     | 
    
         
            +
                    display_name = discover_available_providers.invert.fetch(provider_name, provider_name)
         
     | 
| 
      
 1079 
     | 
    
         
            +
                    prompt.say("  • #{action_word.capitalize} provider '#{display_name}' (#{provider_name}) with billing type '#{provider_type}' and model family '#{model_family}'")
         
     | 
| 
      
 1080 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1081 
     | 
    
         
            +
             
     | 
| 
      
 1082 
     | 
    
         
            +
                  def edit_provider_configuration(provider_name)
         
     | 
| 
      
 1083 
     | 
    
         
            +
                    existing = get([:providers, provider_name.to_sym]) || {}
         
     | 
| 
      
 1084 
     | 
    
         
            +
                    prompt.say("\n🔧 Editing provider '#{provider_name}' (current: type=#{existing[:type] || "unset"}, model_family=#{existing[:model_family] || "unset"})")
         
     | 
| 
      
 1085 
     | 
    
         
            +
                    new_type = ask_provider_billing_type_with_default(provider_name, existing[:type])
         
     | 
| 
      
 1086 
     | 
    
         
            +
                    new_family = ask_model_family(provider_name, existing[:model_family] || "auto")
         
     | 
| 
      
 1087 
     | 
    
         
            +
                    set([:providers, provider_name.to_sym], {type: new_type, model_family: new_family})
         
     | 
| 
      
 1088 
     | 
    
         
            +
                    # Normalize immediately so tests relying on canonical value see 'claude' rather than label
         
     | 
| 
      
 1089 
     | 
    
         
            +
                    normalize_existing_model_families!
         
     | 
| 
      
 1090 
     | 
    
         
            +
                    prompt.ok("Updated '#{provider_name}' → type=#{new_type}, model_family=#{new_family}")
         
     | 
| 
       920 
1091 
     | 
    
         
             
                  end
         
     | 
| 
       921 
1092 
     | 
    
         | 
| 
       922 
1093 
     | 
    
         
             
                  def ask_provider_billing_type(provider_name)
         
     | 
| 
       923 
     | 
    
         
            -
                     
     | 
| 
       924 
     | 
    
         
            -
             
     | 
| 
       925 
     | 
    
         
            -
             
     | 
| 
       926 
     | 
    
         
            -
             
     | 
| 
       927 
     | 
    
         
            -
             
     | 
| 
      
 1094 
     | 
    
         
            +
                    ask_provider_billing_type_with_default(provider_name, nil)
         
     | 
| 
      
 1095 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1096 
     | 
    
         
            +
             
     | 
| 
      
 1097 
     | 
    
         
            +
                  BILLING_TYPE_CHOICES = [
         
     | 
| 
      
 1098 
     | 
    
         
            +
                    ["Subscription / flat-rate", "subscription"],
         
     | 
| 
      
 1099 
     | 
    
         
            +
                    ["Usage-based / metered (API)", "usage_based"],
         
     | 
| 
      
 1100 
     | 
    
         
            +
                    ["Passthrough / local (no billing)", "passthrough"]
         
     | 
| 
      
 1101 
     | 
    
         
            +
                  ].freeze
         
     | 
| 
      
 1102 
     | 
    
         
            +
             
     | 
| 
      
 1103 
     | 
    
         
            +
                  def ask_provider_billing_type_with_default(provider_name, default_value)
         
     | 
| 
      
 1104 
     | 
    
         
            +
                    default_label = BILLING_TYPE_CHOICES.find { |label, value| value == default_value }&.first
         
     | 
| 
      
 1105 
     | 
    
         
            +
                    suffix = default_value ? " (current: #{default_value})" : ""
         
     | 
| 
      
 1106 
     | 
    
         
            +
                    prompt.select("Billing model for #{provider_name}:#{suffix}", default: default_label) do |menu|
         
     | 
| 
      
 1107 
     | 
    
         
            +
                      BILLING_TYPE_CHOICES.each do |label, value|
         
     | 
| 
      
 1108 
     | 
    
         
            +
                        menu.choice(label, value)
         
     | 
| 
      
 1109 
     | 
    
         
            +
                      end
         
     | 
| 
       928 
1110 
     | 
    
         
             
                    end
         
     | 
| 
       929 
1111 
     | 
    
         
             
                  end
         
     | 
| 
       930 
1112 
     | 
    
         | 
| 
      
 1113 
     | 
    
         
            +
                  MODEL_FAMILY_CHOICES = [
         
     | 
| 
      
 1114 
     | 
    
         
            +
                    ["Auto (let provider decide)", "auto"],
         
     | 
| 
      
 1115 
     | 
    
         
            +
                    ["OpenAI o-series (reasoning models)", "openai_o"],
         
     | 
| 
      
 1116 
     | 
    
         
            +
                    ["Anthropic Claude (balanced)", "claude"],
         
     | 
| 
      
 1117 
     | 
    
         
            +
                    ["Mistral (European/open)", "mistral"],
         
     | 
| 
      
 1118 
     | 
    
         
            +
                    ["Local LLM (self-hosted)", "local"]
         
     | 
| 
      
 1119 
     | 
    
         
            +
                  ].freeze
         
     | 
| 
      
 1120 
     | 
    
         
            +
             
     | 
| 
       931 
1121 
     | 
    
         
             
                  def ask_model_family(provider_name, default = "auto")
         
     | 
| 
       932 
     | 
    
         
            -
                     
     | 
| 
       933 
     | 
    
         
            -
             
     | 
| 
       934 
     | 
    
         
            -
             
     | 
| 
       935 
     | 
    
         
            -
             
     | 
| 
       936 
     | 
    
         
            -
             
     | 
| 
       937 
     | 
    
         
            -
                       
     | 
| 
      
 1122 
     | 
    
         
            +
                    # TTY::Prompt validates defaults against the displayed choice labels, not values.
         
     | 
| 
      
 1123 
     | 
    
         
            +
                    # Map the value default (e.g. "auto") to its corresponding label.
         
     | 
| 
      
 1124 
     | 
    
         
            +
                    default_label = MODEL_FAMILY_CHOICES.find { |label, value| value == default }&.first
         
     | 
| 
      
 1125 
     | 
    
         
            +
             
     | 
| 
      
 1126 
     | 
    
         
            +
                    prompt.select("Preferred model family for #{provider_name}:", default: default_label) do |menu|
         
     | 
| 
      
 1127 
     | 
    
         
            +
                      MODEL_FAMILY_CHOICES.each do |label, value|
         
     | 
| 
      
 1128 
     | 
    
         
            +
                        menu.choice(label, value)
         
     | 
| 
      
 1129 
     | 
    
         
            +
                      end
         
     | 
| 
      
 1130 
     | 
    
         
            +
                    end
         
     | 
| 
      
 1131 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1132 
     | 
    
         
            +
             
     | 
| 
      
 1133 
     | 
    
         
            +
                  # Canonicalization helpers ------------------------------------------------
         
     | 
| 
      
 1134 
     | 
    
         
            +
                  MODEL_FAMILY_LABEL_TO_VALUE = MODEL_FAMILY_CHOICES.each_with_object({}) do |(label, value), h|
         
     | 
| 
      
 1135 
     | 
    
         
            +
                    h[label] = value
         
     | 
| 
      
 1136 
     | 
    
         
            +
                  end.freeze
         
     | 
| 
      
 1137 
     | 
    
         
            +
                  MODEL_FAMILY_VALUES = MODEL_FAMILY_CHOICES.map { |(_, value)| value }.freeze
         
     | 
| 
      
 1138 
     | 
    
         
            +
             
     | 
| 
      
 1139 
     | 
    
         
            +
                  def normalize_model_family(value)
         
     | 
| 
      
 1140 
     | 
    
         
            +
                    return "auto" if value.nil? || value.to_s.strip.empty?
         
     | 
| 
      
 1141 
     | 
    
         
            +
                    # Already a canonical value
         
     | 
| 
      
 1142 
     | 
    
         
            +
                    return value if MODEL_FAMILY_VALUES.include?(value)
         
     | 
| 
      
 1143 
     | 
    
         
            +
                    # Try label -> value
         
     | 
| 
      
 1144 
     | 
    
         
            +
                    mapped = MODEL_FAMILY_LABEL_TO_VALUE[value]
         
     | 
| 
      
 1145 
     | 
    
         
            +
                    return mapped if mapped
         
     | 
| 
      
 1146 
     | 
    
         
            +
                    # Unknown legacy entry -> fallback to auto
         
     | 
| 
      
 1147 
     | 
    
         
            +
                    "auto"
         
     | 
| 
      
 1148 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1149 
     | 
    
         
            +
             
     | 
| 
      
 1150 
     | 
    
         
            +
                  def normalize_existing_model_families!
         
     | 
| 
      
 1151 
     | 
    
         
            +
                    providers_cfg = @config[:providers]
         
     | 
| 
      
 1152 
     | 
    
         
            +
                    return unless providers_cfg.is_a?(Hash)
         
     | 
| 
      
 1153 
     | 
    
         
            +
                    providers_cfg.each do |prov_name, prov_cfg|
         
     | 
| 
      
 1154 
     | 
    
         
            +
                      next unless prov_cfg.is_a?(Hash)
         
     | 
| 
      
 1155 
     | 
    
         
            +
                      mf = prov_cfg[:model_family]
         
     | 
| 
      
 1156 
     | 
    
         
            +
                      # Normalize and write back only if different to avoid unnecessary YAML churn
         
     | 
| 
      
 1157 
     | 
    
         
            +
                      normalized = normalize_model_family(mf)
         
     | 
| 
      
 1158 
     | 
    
         
            +
                      prov_cfg[:model_family] = normalized
         
     | 
| 
       938 
1159 
     | 
    
         
             
                    end
         
     | 
| 
       939 
1160 
     | 
    
         
             
                  end
         
     | 
| 
       940 
1161 
     | 
    
         | 
    
        data/lib/aidp/version.rb
    CHANGED
    
    
| 
         @@ -18,7 +18,7 @@ module Aidp 
     | 
|
| 
       18 
18 
     | 
    
         | 
| 
       19 
19 
     | 
    
         
             
                  class ConversationError < StandardError; end
         
     | 
| 
       20 
20 
     | 
    
         | 
| 
       21 
     | 
    
         
            -
                  def initialize(project_dir, prompt: nil, use_enhanced_input: true)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  def initialize(project_dir, prompt: nil, use_enhanced_input: true, verbose: false)
         
     | 
| 
       22 
22 
     | 
    
         
             
                    @project_dir = project_dir
         
     | 
| 
       23 
23 
     | 
    
         | 
| 
       24 
24 
     | 
    
         
             
                    # Use EnhancedInput with Reline for full readline-style key bindings
         
     | 
| 
         @@ -32,6 +32,9 @@ module Aidp 
     | 
|
| 
       32 
32 
     | 
    
         
             
                    @provider_manager = Aidp::Harness::ProviderManager.new(@config_manager, prompt: @prompt)
         
     | 
| 
       33 
33 
     | 
    
         
             
                    @conversation_history = []
         
     | 
| 
       34 
34 
     | 
    
         
             
                    @user_input = {}
         
     | 
| 
      
 35 
     | 
    
         
            +
                    @invalid_planning_responses = 0
         
     | 
| 
      
 36 
     | 
    
         
            +
                    @verbose = verbose
         
     | 
| 
      
 37 
     | 
    
         
            +
                    @debug_env = ENV["DEBUG"] == "1" || ENV["DEBUG"] == "2"
         
     | 
| 
       35 
38 
     | 
    
         
             
                  end
         
     | 
| 
       36 
39 
     | 
    
         | 
| 
       37 
40 
     | 
    
         
             
                  # Main entry point for guided workflow selection
         
     | 
| 
         @@ -79,6 +82,7 @@ module Aidp 
     | 
|
| 
       79 
82 
     | 
    
         
             
                      iteration += 1
         
     | 
| 
       80 
83 
     | 
    
         
             
                      # Ask AI for next question based on current plan
         
     | 
| 
       81 
84 
     | 
    
         
             
                      question_response = get_planning_questions(plan)
         
     | 
| 
      
 85 
     | 
    
         
            +
                      emit_verbose_iteration(plan, question_response)
         
     | 
| 
       82 
86 
     | 
    
         | 
| 
       83 
87 
     | 
    
         
             
                      # Debug: show raw provider response and parsed result
         
     | 
| 
       84 
88 
     | 
    
         
             
                      debug_log("Planning iteration #{iteration} provider response", level: :debug, data: {
         
     | 
| 
         @@ -131,9 +135,10 @@ module Aidp 
     | 
|
| 
       131 
135 
     | 
    
         
             
                    end
         
     | 
| 
       132 
136 
     | 
    
         | 
| 
       133 
137 
     | 
    
         
             
                    response = call_provider_for_analysis(system_prompt, user_prompt)
         
     | 
| 
       134 
     | 
    
         
            -
                    parsed =  
     | 
| 
      
 138 
     | 
    
         
            +
                    parsed = safe_parse_planning_response(response)
         
     | 
| 
       135 
139 
     | 
    
         
             
                    # Attach raw response for debug
         
     | 
| 
       136 
140 
     | 
    
         
             
                    parsed[:raw_response] = response
         
     | 
| 
      
 141 
     | 
    
         
            +
                    emit_verbose_raw_prompt(system_prompt, user_prompt, response)
         
     | 
| 
       137 
142 
     | 
    
         
             
                    parsed
         
     | 
| 
       138 
143 
     | 
    
         
             
                  end
         
     | 
| 
       139 
144 
     | 
    
         | 
| 
         @@ -239,16 +244,64 @@ module Aidp 
     | 
|
| 
       239 
244 
     | 
    
         | 
| 
       240 
245 
     | 
    
         
             
                      if classified && attempts < max_attempts
         
     | 
| 
       241 
246 
     | 
    
         
             
                        display_message("⚠️  Provider '#{provider_name}' #{classified.tr("_", " ")} – attempting fallback...", type: :warning)
         
     | 
| 
       242 
     | 
    
         
            -
                         
     | 
| 
       243 
     | 
    
         
            -
             
     | 
| 
       244 
     | 
    
         
            -
                           
     | 
| 
       245 
     | 
    
         
            -
             
     | 
| 
      
 247 
     | 
    
         
            +
                        if @provider_manager.respond_to?(:switch_provider_for_error)
         
     | 
| 
      
 248 
     | 
    
         
            +
                          switched = @provider_manager.switch_provider_for_error(classified, stderr: message)
         
     | 
| 
      
 249 
     | 
    
         
            +
                          if switched && switched != provider_name
         
     | 
| 
      
 250 
     | 
    
         
            +
                            display_message("↩️  Switched to provider '#{switched}'", type: :info)
         
     | 
| 
      
 251 
     | 
    
         
            +
                            retry
         
     | 
| 
      
 252 
     | 
    
         
            +
                          elsif switched == provider_name
         
     | 
| 
      
 253 
     | 
    
         
            +
                            # ProviderManager could not advance; mark current as rate limited to encourage next attempt to move on.
         
     | 
| 
      
 254 
     | 
    
         
            +
                            Aidp.logger.debug("guided_agent", "provider_switch_noop", provider: provider_name, reason: classified)
         
     | 
| 
      
 255 
     | 
    
         
            +
                            if @provider_manager.respond_to?(:mark_rate_limited)
         
     | 
| 
      
 256 
     | 
    
         
            +
                              @provider_manager.mark_rate_limited(provider_name)
         
     | 
| 
      
 257 
     | 
    
         
            +
                              next_provider = @provider_manager.switch_provider("rate_limit_forced", previous_error: message)
         
     | 
| 
      
 258 
     | 
    
         
            +
                              if next_provider && next_provider != provider_name
         
     | 
| 
      
 259 
     | 
    
         
            +
                                display_message("↩️  Switched to provider '#{next_provider}' (forced)", type: :info)
         
     | 
| 
      
 260 
     | 
    
         
            +
                                retry
         
     | 
| 
      
 261 
     | 
    
         
            +
                              end
         
     | 
| 
      
 262 
     | 
    
         
            +
                            end
         
     | 
| 
      
 263 
     | 
    
         
            +
                          end
         
     | 
| 
       246 
264 
     | 
    
         
             
                        end
         
     | 
| 
       247 
265 
     | 
    
         
             
                      end
         
     | 
| 
       248 
266 
     | 
    
         
             
                      raise
         
     | 
| 
       249 
267 
     | 
    
         
             
                    end
         
     | 
| 
       250 
268 
     | 
    
         
             
                  end
         
     | 
| 
       251 
269 
     | 
    
         | 
| 
      
 270 
     | 
    
         
            +
                  # Verbose output helpers
         
     | 
| 
      
 271 
     | 
    
         
            +
                  def emit_verbose_raw_prompt(system_prompt, user_prompt, raw_response)
         
     | 
| 
      
 272 
     | 
    
         
            +
                    return unless @verbose || @debug_env
         
     | 
| 
      
 273 
     | 
    
         
            +
                    if @verbose
         
     | 
| 
      
 274 
     | 
    
         
            +
                      display_message("\n--- Prompt Sent (Planning) ---", type: :muted)
         
     | 
| 
      
 275 
     | 
    
         
            +
                      display_message(system_prompt.strip, type: :muted)
         
     | 
| 
      
 276 
     | 
    
         
            +
                      display_message(user_prompt.strip, type: :muted)
         
     | 
| 
      
 277 
     | 
    
         
            +
                      display_message("--- Raw Provider Response ---", type: :muted)
         
     | 
| 
      
 278 
     | 
    
         
            +
                      display_message(raw_response.to_s.strip, type: :muted)
         
     | 
| 
      
 279 
     | 
    
         
            +
                      display_message("------------------------------\n", type: :muted)
         
     | 
| 
      
 280 
     | 
    
         
            +
                    elsif @debug_env
         
     | 
| 
      
 281 
     | 
    
         
            +
                      Aidp.logger.debug("guided_agent", "planning_prompt", system: system_prompt.strip, user: user_prompt.strip)
         
     | 
| 
      
 282 
     | 
    
         
            +
                      Aidp.logger.debug("guided_agent", "planning_raw_response", raw: raw_response.to_s.strip)
         
     | 
| 
      
 283 
     | 
    
         
            +
                    end
         
     | 
| 
      
 284 
     | 
    
         
            +
                  rescue => e
         
     | 
| 
      
 285 
     | 
    
         
            +
                    Aidp.logger.warn("guided_agent", "Failed verbose prompt emit", error: e.message)
         
     | 
| 
      
 286 
     | 
    
         
            +
                  end
         
     | 
| 
      
 287 
     | 
    
         
            +
             
     | 
| 
      
 288 
     | 
    
         
            +
                  def emit_verbose_iteration(plan, question_response)
         
     | 
| 
      
 289 
     | 
    
         
            +
                    return unless @verbose || @debug_env
         
     | 
| 
      
 290 
     | 
    
         
            +
                    summary = {complete: question_response[:complete], questions: question_response[:questions], reasoning: question_response[:reasoning], error: question_response[:error]}
         
     | 
| 
      
 291 
     | 
    
         
            +
                    if @verbose
         
     | 
| 
      
 292 
     | 
    
         
            +
                      display_message("\n=== Planning Iteration Summary ===", type: :info)
         
     | 
| 
      
 293 
     | 
    
         
            +
                      display_message("Questions: #{(summary[:questions] || []).join(" | ")}", type: :info)
         
     | 
| 
      
 294 
     | 
    
         
            +
                      display_message("Complete? #{summary[:complete]}", type: :info)
         
     | 
| 
      
 295 
     | 
    
         
            +
                      display_message("Reasoning: #{summary[:reasoning]}", type: :muted) if summary[:reasoning]
         
     | 
| 
      
 296 
     | 
    
         
            +
                      display_message("Error: #{summary[:error]}", type: :warning) if summary[:error]
         
     | 
| 
      
 297 
     | 
    
         
            +
                      display_message("=================================", type: :info)
         
     | 
| 
      
 298 
     | 
    
         
            +
                    elsif @debug_env
         
     | 
| 
      
 299 
     | 
    
         
            +
                      Aidp.logger.debug("guided_agent", "iteration_summary", summary: summary, plan_progress_keys: plan.keys)
         
     | 
| 
      
 300 
     | 
    
         
            +
                    end
         
     | 
| 
      
 301 
     | 
    
         
            +
                  rescue => e
         
     | 
| 
      
 302 
     | 
    
         
            +
                    Aidp.logger.warn("guided_agent", "Failed verbose iteration emit", error: e.message)
         
     | 
| 
      
 303 
     | 
    
         
            +
                  end
         
     | 
| 
      
 304 
     | 
    
         
            +
             
     | 
| 
       252 
305 
     | 
    
         
             
                  def validate_provider_configuration!
         
     | 
| 
       253 
306 
     | 
    
         
             
                    configured = @provider_manager.configured_providers
         
     | 
| 
       254 
307 
     | 
    
         
             
                    if configured.nil? || configured.empty?
         
     | 
| 
         @@ -307,20 +360,53 @@ module Aidp 
     | 
|
| 
       307 
360 
     | 
    
         
             
                    json_match = response_text.match(/```json\s*(\{.*?\})\s*```/m) ||
         
     | 
| 
       308 
361 
     | 
    
         
             
                      response_text.match(/(\{.*\})/m)
         
     | 
| 
       309 
362 
     | 
    
         | 
| 
       310 
     | 
    
         
            -
                    unless json_match
         
     | 
| 
       311 
     | 
    
         
            -
                      return {complete: false, questions: ["Could you tell me more about your requirements?"]}
         
     | 
| 
       312 
     | 
    
         
            -
                    end
         
     | 
| 
       313 
     | 
    
         
            -
             
     | 
| 
      
 363 
     | 
    
         
            +
                    return {error: :invalid_format} unless json_match
         
     | 
| 
       314 
364 
     | 
    
         
             
                    JSON.parse(json_match[1], symbolize_names: true)
         
     | 
| 
       315 
365 
     | 
    
         
             
                  rescue JSON::ParserError
         
     | 
| 
       316 
     | 
    
         
            -
                    { 
     | 
| 
      
 366 
     | 
    
         
            +
                    {error: :invalid_format}
         
     | 
| 
      
 367 
     | 
    
         
            +
                  end
         
     | 
| 
      
 368 
     | 
    
         
            +
             
     | 
| 
      
 369 
     | 
    
         
            +
                  # Provides structured fallback sequence when provider keeps returning invalid planning JSON.
         
     | 
| 
      
 370 
     | 
    
         
            +
                  # After exceeding sequence length, switches to manual entry question.
         
     | 
| 
      
 371 
     | 
    
         
            +
                  def safe_parse_planning_response(response_text)
         
     | 
| 
      
 372 
     | 
    
         
            +
                    parsed = parse_planning_response(response_text)
         
     | 
| 
      
 373 
     | 
    
         
            +
                    return parsed unless parsed.is_a?(Hash) && parsed[:error] == :invalid_format
         
     | 
| 
      
 374 
     | 
    
         
            +
             
     | 
| 
      
 375 
     | 
    
         
            +
                    @invalid_planning_responses += 1
         
     | 
| 
      
 376 
     | 
    
         
            +
                    fallback_sequence = [
         
     | 
| 
      
 377 
     | 
    
         
            +
                      "Provide scope (key features) and primary users.",
         
     | 
| 
      
 378 
     | 
    
         
            +
                      "List 3-5 key functional requirements and any technical constraints.",
         
     | 
| 
      
 379 
     | 
    
         
            +
                      "Supply any non-functional requirements (performance/security) or type 'skip'."
         
     | 
| 
      
 380 
     | 
    
         
            +
                    ]
         
     | 
| 
      
 381 
     | 
    
         
            +
             
     | 
| 
      
 382 
     | 
    
         
            +
                    if @invalid_planning_responses <= fallback_sequence.size
         
     | 
| 
      
 383 
     | 
    
         
            +
                      {complete: false, questions: [fallback_sequence[@invalid_planning_responses - 1]], reasoning: "Fallback due to invalid provider response (format)", error: :fallback}
         
     | 
| 
      
 384 
     | 
    
         
            +
                    else
         
     | 
| 
      
 385 
     | 
    
         
            +
                      display_message("[ERROR] Provider returned invalid planning JSON #{@invalid_planning_responses} times. Enter combined plan details manually.", type: :error)
         
     | 
| 
      
 386 
     | 
    
         
            +
                      {complete: false, questions: ["Enter plan details manually (features; users; requirements; constraints) or type 'skip'"], reasoning: "Manual recovery mode", error: :manual_recovery}
         
     | 
| 
      
 387 
     | 
    
         
            +
                    end
         
     | 
| 
       317 
388 
     | 
    
         
             
                  end
         
     | 
| 
       318 
389 
     | 
    
         | 
| 
       319 
390 
     | 
    
         
             
                  def update_plan_from_answer(plan, question, answer)
         
     | 
| 
       320 
391 
     | 
    
         
             
                    # Simple heuristic-based plan updates
         
     | 
| 
       321 
392 
     | 
    
         
             
                    # In a more sophisticated implementation, use AI to categorize answers
         
     | 
| 
       322 
393 
     | 
    
         | 
| 
       323 
     | 
    
         
            -
                     
     | 
| 
      
 394 
     | 
    
         
            +
                    # IMPORTANT: Check manual recovery sentinel prompt first so it isn't misclassified
         
     | 
| 
      
 395 
     | 
    
         
            +
                    # by broader keyword heuristics (e.g., it contains the word 'users').
         
     | 
| 
      
 396 
     | 
    
         
            +
                    if question.start_with?("Enter plan details manually")
         
     | 
| 
      
 397 
     | 
    
         
            +
                      unless answer.strip.downcase == "skip"
         
     | 
| 
      
 398 
     | 
    
         
            +
                        parts = answer.split(/;|\|/).map(&:strip).reject(&:empty?)
         
     | 
| 
      
 399 
     | 
    
         
            +
                        features, users, requirements, constraints = parts
         
     | 
| 
      
 400 
     | 
    
         
            +
                        plan[:scope][:included] ||= []
         
     | 
| 
      
 401 
     | 
    
         
            +
                        plan[:scope][:included] << features if features
         
     | 
| 
      
 402 
     | 
    
         
            +
                        plan[:users][:personas] ||= []
         
     | 
| 
      
 403 
     | 
    
         
            +
                        plan[:users][:personas] << users if users
         
     | 
| 
      
 404 
     | 
    
         
            +
                        plan[:requirements][:functional] ||= []
         
     | 
| 
      
 405 
     | 
    
         
            +
                        plan[:requirements][:functional] << requirements if requirements
         
     | 
| 
      
 406 
     | 
    
         
            +
                        plan[:constraints][:technical] ||= []
         
     | 
| 
      
 407 
     | 
    
         
            +
                        plan[:constraints][:technical] << constraints if constraints
         
     | 
| 
      
 408 
     | 
    
         
            +
                      end
         
     | 
| 
      
 409 
     | 
    
         
            +
                    elsif question.downcase.include?("scope") || question.downcase.include?("include")
         
     | 
| 
       324 
410 
     | 
    
         
             
                      plan[:scope][:included] ||= []
         
     | 
| 
       325 
411 
     | 
    
         
             
                      plan[:scope][:included] << answer
         
     | 
| 
       326 
412 
     | 
    
         
             
                    elsif question.downcase.include?("user") || question.downcase.include?("who")
         
     | 
| 
         @@ -338,7 +424,6 @@ module Aidp 
     | 
|
| 
       338 
424 
     | 
    
         
             
                    elsif question.downcase.include?("complete") || question.downcase.include?("done") || question.downcase.include?("success")
         
     | 
| 
       339 
425 
     | 
    
         
             
                      plan[:completion_criteria] << answer
         
     | 
| 
       340 
426 
     | 
    
         
             
                    else
         
     | 
| 
       341 
     | 
    
         
            -
                      # General information
         
     | 
| 
       342 
427 
     | 
    
         
             
                      plan[:additional_context] ||= []
         
     | 
| 
       343 
428 
     | 
    
         
             
                      plan[:additional_context] << {question: question, answer: answer}
         
     | 
| 
       344 
429 
     | 
    
         
             
                    end
         
     | 
    
        data/lib/aidp.rb
    CHANGED
    
    | 
         @@ -7,6 +7,7 @@ require_relative "aidp/core_ext/class_attribute" 
     | 
|
| 
       7 
7 
     | 
    
         
             
            require_relative "aidp/version"
         
     | 
| 
       8 
8 
     | 
    
         
             
            require_relative "aidp/config"
         
     | 
| 
       9 
9 
     | 
    
         
             
            require_relative "aidp/util"
         
     | 
| 
      
 10 
     | 
    
         
            +
            require_relative "aidp/rescue_logging"
         
     | 
| 
       10 
11 
     | 
    
         
             
            require_relative "aidp/message_display"
         
     | 
| 
       11 
12 
     | 
    
         
             
            require_relative "aidp/concurrency"
         
     | 
| 
       12 
13 
     | 
    
         
             
            require_relative "aidp/setup/wizard"
         
     |