aidp 0.28.0 → 0.30.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.
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+
5
+ module Aidp
6
+ module Metadata
7
+ # Validates tool metadata and detects issues
8
+ #
9
+ # Performs validation checks on tool metadata including:
10
+ # - Required field presence
11
+ # - Field type validation
12
+ # - Duplicate ID detection
13
+ # - Dependency resolution
14
+ # - Version format validation
15
+ #
16
+ # @example Validating a collection of tools
17
+ # validator = Validator.new(tools)
18
+ # results = validator.validate_all
19
+ # results.each { |r| puts "#{r[:file]}: #{r[:errors].join(", ")}" }
20
+ class Validator
21
+ # Validation result structure
22
+ ValidationResult = Struct.new(:tool_id, :file_path, :valid, :errors, :warnings, keyword_init: true)
23
+
24
+ # Initialize validator with tool metadata collection
25
+ #
26
+ # @param tools [Array<ToolMetadata>] Tools to validate
27
+ def initialize(tools = [])
28
+ @tools = tools
29
+ @errors_by_id = {}
30
+ @warnings_by_id = {}
31
+ end
32
+
33
+ # Validate all tools
34
+ #
35
+ # @return [Array<ValidationResult>] Validation results for each tool
36
+ def validate_all
37
+ Aidp.log_debug("metadata", "Validating all tools", count: @tools.size)
38
+
39
+ results = @tools.map do |tool|
40
+ validate_tool(tool)
41
+ end
42
+
43
+ # Cross-tool validations
44
+ validate_duplicate_ids(results)
45
+ validate_dependencies(results)
46
+
47
+ Aidp.log_info(
48
+ "metadata",
49
+ "Validation complete",
50
+ total: results.size,
51
+ valid: results.count(&:valid),
52
+ invalid: results.count { |r| !r.valid }
53
+ )
54
+
55
+ results
56
+ end
57
+
58
+ # Validate a single tool
59
+ #
60
+ # @param tool [ToolMetadata] Tool to validate
61
+ # @return [ValidationResult] Validation result
62
+ def validate_tool(tool)
63
+ errors = []
64
+ warnings = []
65
+
66
+ # Tool metadata validates itself on initialization
67
+ # Here we add additional cross-cutting validations
68
+
69
+ # Check for empty arrays in key fields
70
+ if tool.applies_to.empty? && tool.work_unit_types.empty?
71
+ warnings << "No applies_to tags or work_unit_types specified (tool may not be discoverable)"
72
+ end
73
+
74
+ # Check for deprecated fields or patterns
75
+ validate_deprecated_patterns(tool, warnings)
76
+
77
+ # Check for experimental tools
78
+ if tool.experimental
79
+ warnings << "Tool is marked as experimental"
80
+ end
81
+
82
+ # Check content length
83
+ if tool.content.length < 100
84
+ warnings << "Tool content is very short (#{tool.content.length} characters)"
85
+ end
86
+
87
+ ValidationResult.new(
88
+ tool_id: tool.id,
89
+ file_path: tool.source_path,
90
+ valid: errors.empty?,
91
+ errors: errors,
92
+ warnings: warnings
93
+ )
94
+ rescue Aidp::Errors::ValidationError => e
95
+ # Catch validation errors from tool initialization
96
+ ValidationResult.new(
97
+ tool_id: tool&.id || "unknown",
98
+ file_path: tool&.source_path || "unknown",
99
+ valid: false,
100
+ errors: [e.message],
101
+ warnings: []
102
+ )
103
+ end
104
+
105
+ # Validate for duplicate IDs across tools
106
+ #
107
+ # @param results [Array<ValidationResult>] Validation results
108
+ def validate_duplicate_ids(results)
109
+ ids = @tools.map(&:id)
110
+ duplicates = ids.tally.select { |_, count| count > 1 }.keys
111
+
112
+ return if duplicates.empty?
113
+
114
+ duplicates.each do |dup_id|
115
+ matching_tools = @tools.select { |t| t.id == dup_id }
116
+ matching_tools.each do |tool|
117
+ result = results.find { |r| r.tool_id == tool.id && r.file_path == tool.source_path }
118
+ next unless result
119
+
120
+ paths = matching_tools.map(&:source_path).join(", ")
121
+ result.errors << "Duplicate ID '#{dup_id}' found in: #{paths}"
122
+ result.valid = false
123
+ end
124
+ end
125
+
126
+ Aidp.log_warn("metadata", "Duplicate IDs detected", duplicates: duplicates)
127
+ end
128
+
129
+ # Validate tool dependencies are satisfied
130
+ #
131
+ # @param results [Array<ValidationResult>] Validation results
132
+ def validate_dependencies(results)
133
+ available_ids = @tools.map(&:id).to_set
134
+
135
+ @tools.each do |tool|
136
+ next if tool.dependencies.empty?
137
+
138
+ tool.dependencies.each do |dep_id|
139
+ next if available_ids.include?(dep_id)
140
+
141
+ result = results.find { |r| r.tool_id == tool.id && r.file_path == tool.source_path }
142
+ next unless result
143
+
144
+ result.errors << "Missing dependency: '#{dep_id}'"
145
+ result.valid = false
146
+ end
147
+ end
148
+ end
149
+
150
+ # Check for deprecated patterns in tool metadata
151
+ #
152
+ # @param tool [ToolMetadata] Tool to check
153
+ # @param warnings [Array<String>] Warnings array to append to
154
+ def validate_deprecated_patterns(tool, warnings)
155
+ # Check for legacy field usage (this would be expanded based on actual deprecations)
156
+ # For now, this is a placeholder for future deprecation warnings
157
+ end
158
+
159
+ # Write validation errors to log file
160
+ #
161
+ # @param results [Array<ValidationResult>] Validation results
162
+ # @param log_path [String] Path to error log file
163
+ def write_error_log(results, log_path)
164
+ Aidp.log_debug("metadata", "Writing error log", path: log_path)
165
+
166
+ errors = results.reject(&:valid)
167
+ return if errors.empty?
168
+
169
+ File.open(log_path, "w") do |f|
170
+ f.puts "# Metadata Validation Errors"
171
+ f.puts "# Generated: #{Time.now.iso8601}"
172
+ f.puts
173
+
174
+ errors.each do |result|
175
+ f.puts "## #{result.tool_id} (#{result.file_path})"
176
+ f.puts
177
+ result.errors.each { |err| f.puts "- ERROR: #{err}" }
178
+ result.warnings.each { |warn| f.puts "- WARNING: #{warn}" }
179
+ f.puts
180
+ end
181
+ end
182
+
183
+ Aidp.log_info("metadata", "Wrote error log", path: log_path, error_count: errors.size)
184
+ end
185
+ end
186
+ end
187
+ end
@@ -75,7 +75,7 @@ module Aidp
75
75
  end
76
76
  end
77
77
 
78
- def send_message(prompt:, session: nil)
78
+ def send_message(prompt:, session: nil, options: {})
79
79
  raise "aider CLI not available" unless self.class.available?
80
80
 
81
81
  # Smart timeout calculation (store prompt length for adaptive logic)
@@ -3,15 +3,23 @@
3
3
  require "json"
4
4
  require_relative "base"
5
5
  require_relative "../debug_mixin"
6
+ require_relative "../harness/deprecation_cache"
6
7
 
7
8
  module Aidp
8
9
  module Providers
9
10
  class Anthropic < Base
10
11
  include Aidp::DebugMixin
11
12
 
13
+ attr_reader :model
14
+
12
15
  # Model name pattern for Anthropic Claude models
13
16
  MODEL_PATTERN = /^claude-[\d.-]+-(?:opus|sonnet|haiku)(?:-\d{8})?$/i
14
17
 
18
+ # Get deprecation cache instance (lazy loaded)
19
+ def self.deprecation_cache
20
+ @deprecation_cache ||= Aidp::Harness::DeprecationCache.new
21
+ end
22
+
15
23
  def self.available?
16
24
  !!Aidp::Util.which("claude")
17
25
  end
@@ -210,6 +218,56 @@ module Aidp
210
218
  ["--dangerously-skip-permissions"]
211
219
  end
212
220
 
221
+ # Classify provider error using string matching
222
+ #
223
+ # ZFC EXCEPTION: Cannot use AI to classify provider errors because:
224
+ # 1. The failing provider IS the AI we'd use for classification (circular dependency)
225
+ # 2. Provider may be rate-limited, down, or misconfigured
226
+ # 3. Error classification must work even when AI unavailable
227
+ #
228
+ # This is a legitimate exception to ZFC principles per LLM_STYLE_GUIDE:
229
+ # "Structural safety checks" are allowed in code when AI cannot be used.
230
+ #
231
+ # @param error_message [String] The error message to classify
232
+ # @return [Hash] Classification result with :type, :is_deprecation, :is_rate_limit, :confidence
233
+ def self.classify_provider_error(error_message)
234
+ msg_lower = error_message.downcase
235
+
236
+ # Use simple string.include? checks (not regex) to avoid ReDoS vulnerabilities
237
+ is_rate_limit = msg_lower.include?("rate limit") || msg_lower.include?("session limit")
238
+ is_deprecation = msg_lower.include?("deprecat") || msg_lower.include?("end-of-life")
239
+ is_auth_error = msg_lower.include?("auth") && (msg_lower.include?("expired") || msg_lower.include?("invalid"))
240
+
241
+ # Determine primary type
242
+ type = if is_rate_limit
243
+ "rate_limit"
244
+ elsif is_deprecation
245
+ "deprecation"
246
+ elsif is_auth_error
247
+ "auth_error"
248
+ else
249
+ "other"
250
+ end
251
+
252
+ Aidp.log_debug("anthropic", "Provider error classification",
253
+ type: type,
254
+ is_rate_limit: is_rate_limit,
255
+ is_deprecation: is_deprecation,
256
+ is_auth_error: is_auth_error)
257
+
258
+ {
259
+ type: type,
260
+ is_rate_limit: is_rate_limit,
261
+ is_deprecation: is_deprecation,
262
+ is_auth_error: is_auth_error,
263
+ confidence: 0.85, # Good confidence for clear error messages
264
+ reasoning: "Pattern-based classification (ZFC exception: circular dependency)"
265
+ }
266
+ end
267
+
268
+ # Error patterns for classify_error method (legacy support)
269
+ # NOTE: ZFC-based classification preferred - see classify_error_with_zfc
270
+ # These patterns serve as fallback when ZFC is unavailable
213
271
  def error_patterns
214
272
  {
215
273
  rate_limited: [
@@ -245,18 +303,76 @@ module Aidp
245
303
  /not.*found/i,
246
304
  /404/,
247
305
  /bad.*request/i,
248
- /400/
306
+ /400/,
307
+ /model.*deprecated/i,
308
+ /end-of-life/i
249
309
  ]
250
310
  }
251
311
  end
252
312
 
253
- def send_message(prompt:, session: nil)
313
+ # Check if a model is deprecated and return replacement
314
+ # @param model_name [String] The model name to check
315
+ # @return [String, nil] Replacement model name if deprecated, nil otherwise
316
+ def self.check_model_deprecation(model_name)
317
+ deprecation_cache.replacement_for(provider: "anthropic", model_id: model_name)
318
+ end
319
+
320
+ # Find replacement model for deprecated one using RubyLLM registry
321
+ # @param deprecated_model [String] The deprecated model name
322
+ # @param provider_name [String] Provider name for registry lookup
323
+ # @return [String, nil] Latest model in the same family, or configured replacement
324
+ def self.find_replacement_model(deprecated_model, provider_name: "anthropic")
325
+ # First check the deprecation cache for explicit replacement
326
+ replacement = deprecation_cache.replacement_for(provider: provider_name, model_id: deprecated_model)
327
+ return replacement if replacement
328
+
329
+ # Try to find latest model in same family using registry
330
+ require_relative "../harness/ruby_llm_registry" unless defined?(Aidp::Harness::RubyLLMRegistry)
331
+
332
+ begin
333
+ registry = Aidp::Harness::RubyLLMRegistry.new
334
+
335
+ # Search for non-deprecated models in the same family
336
+ # Prefer models without "latest" suffix, sorted by ID (newer dates first)
337
+ models = registry.models_for_provider(provider_name)
338
+ candidates = models.select { |m| m.include?("sonnet") && !deprecation_cache.deprecated?(provider: provider_name, model_id: m) }
339
+
340
+ # Prioritize: versioned models over -latest, newer versions first
341
+ versioned = candidates.reject { |m| m.end_with?("-latest") }.sort.reverse
342
+ latest_models = candidates.select { |m| m.end_with?("-latest") }
343
+
344
+ replacement = versioned.first || latest_models.first
345
+
346
+ if replacement
347
+ Aidp.log_info("anthropic", "Found replacement model",
348
+ deprecated: deprecated_model,
349
+ replacement: replacement)
350
+ end
351
+
352
+ replacement
353
+ rescue => e
354
+ Aidp.log_error("anthropic", "Failed to find replacement model",
355
+ deprecated: deprecated_model,
356
+ error: e.message)
357
+ nil
358
+ end
359
+ end
360
+
361
+ def send_message(prompt:, session: nil, options: {})
254
362
  raise "claude CLI not available" unless self.class.available?
255
363
 
256
- # Smart timeout calculation
257
- timeout_seconds = calculate_timeout
364
+ # Check if current model is deprecated and warn
365
+ if @model && (replacement = self.class.check_model_deprecation(@model))
366
+ Aidp.log_warn("anthropic", "Using deprecated model",
367
+ current: @model,
368
+ replacement: replacement)
369
+ debug_log("⚠️ Model #{@model} is deprecated. Consider upgrading to #{replacement}", level: :warn)
370
+ end
258
371
 
259
- debug_provider("claude", "Starting execution", {timeout: timeout_seconds})
372
+ # Smart timeout calculation with tier awareness
373
+ timeout_seconds = calculate_timeout(options)
374
+
375
+ debug_provider("claude", "Starting execution", {timeout: timeout_seconds, tier: options[:tier]})
260
376
  debug_log("📝 Sending prompt to claude...", level: :info)
261
377
 
262
378
  # Build command arguments
@@ -285,15 +401,72 @@ module Aidp
285
401
  # Detect issues in stdout/stderr (Claude sometimes prints to stdout)
286
402
  combined = [result.out, result.err].compact.join("\n")
287
403
 
288
- # Check for rate limit (Session limit reached)
289
- if combined.match?(/session limit reached/i)
404
+ # Classify provider error using pattern matching
405
+ # ZFC EXCEPTION: Cannot use AI to classify provider's own errors (circular dependency)
406
+ error_classification = self.class.classify_provider_error(combined)
407
+
408
+ Aidp.log_debug("anthropic_provider", "error_classified",
409
+ exit_code: result.exit_status,
410
+ type: error_classification[:type],
411
+ confidence: error_classification[:confidence]) # Check for rate limit
412
+ if error_classification[:is_rate_limit]
290
413
  Aidp.log_debug("anthropic_provider", "rate_limit_detected",
291
414
  exit_code: result.exit_status,
415
+ confidence: error_classification[:confidence],
292
416
  message: combined)
293
417
  notify_rate_limit(combined)
294
418
  error_message = "Rate limit reached for Claude CLI.\n#{combined}"
295
- debug_error(StandardError.new(error_message), {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
296
- raise error_message
419
+ error = RuntimeError.new(error_message)
420
+ debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
421
+ raise error
422
+ end
423
+
424
+ # Check for model deprecation
425
+ if error_classification[:is_deprecation]
426
+ deprecated_model = @model
427
+ Aidp.log_error("anthropic", "Model deprecation detected",
428
+ model: deprecated_model,
429
+ message: combined)
430
+
431
+ # Try to find replacement
432
+ replacement = deprecated_model ? self.class.find_replacement_model(deprecated_model) : nil
433
+
434
+ # Record deprecation in cache for future runs
435
+ if replacement
436
+ self.class.deprecation_cache.add_deprecated_model(
437
+ provider: "anthropic",
438
+ model_id: deprecated_model,
439
+ replacement: replacement,
440
+ reason: combined.lines.first&.strip || "Model deprecated"
441
+ )
442
+
443
+ Aidp.log_info("anthropic", "Auto-upgrading to non-deprecated model",
444
+ old_model: deprecated_model,
445
+ new_model: replacement)
446
+ debug_log("🔄 Upgrading from deprecated model #{deprecated_model} to #{replacement}", level: :info)
447
+
448
+ # Update model and retry
449
+ @model = replacement
450
+
451
+ # Retry with new model
452
+ debug_log("🔄 Retrying with upgraded model: #{replacement}", level: :info)
453
+ return send_message(prompt: prompt, session: session, options: options)
454
+ else
455
+ # Record deprecation even without replacement
456
+ if deprecated_model
457
+ self.class.deprecation_cache.add_deprecated_model(
458
+ provider: "anthropic",
459
+ model_id: deprecated_model,
460
+ replacement: nil,
461
+ reason: combined.lines.first&.strip || "Model deprecated"
462
+ )
463
+ end
464
+
465
+ error_message = "Model '#{deprecated_model}' is deprecated and no replacement found.\n#{combined}"
466
+ error = RuntimeError.new(error_message)
467
+ debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
468
+ raise error
469
+ end
297
470
  end
298
471
 
299
472
  # Check for auth issues
@@ -301,12 +474,15 @@ module Aidp
301
474
  error_message = "Authentication error from Claude CLI: token expired or invalid.\n" \
302
475
  "Run 'claude /login' or refresh credentials.\n" \
303
476
  "Note: Model discovery requires valid authentication."
304
- debug_error(StandardError.new(error_message), {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
305
- raise error_message
477
+ error = RuntimeError.new(error_message)
478
+ debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
479
+ raise error
306
480
  end
307
481
 
308
- debug_error(StandardError.new("claude failed"), {exit_code: result.exit_status, stderr: result.err})
309
- raise "claude failed with exit code #{result.exit_status}: #{result.err}"
482
+ error_message = "claude failed with exit code #{result.exit_status}: #{result.err}"
483
+ error = RuntimeError.new(error_message)
484
+ debug_error(error, {exit_code: result.exit_status, stderr: result.err})
485
+ raise error
310
486
  end
311
487
  rescue => e
312
488
  debug_error(e, {provider: "claude", prompt_length: prompt.length})
@@ -37,6 +37,16 @@ module Aidp
37
37
  TIMEOUT_REFACTORING_RECOMMENDATIONS = 600 # 10 minutes - refactoring
38
38
  TIMEOUT_IMPLEMENTATION = 900 # 15 minutes - implementation (write files, run tests, fix issues)
39
39
 
40
+ # Tier-based timeout multipliers (applied on top of base timeouts)
41
+ # Higher tiers need more time for deeper reasoning
42
+ TIER_TIMEOUT_MULTIPLIERS = {
43
+ "mini" => 1.0, # 5 minutes default (300s base)
44
+ "standard" => 2.0, # 10 minutes default (600s base)
45
+ "thinking" => 6.0, # 30 minutes default (1800s base)
46
+ "pro" => 6.0, # 30 minutes default (1800s base)
47
+ "max" => 12.0 # 60 minutes default (3600s base)
48
+ }.freeze
49
+
40
50
  attr_reader :activity_state, :last_activity_time, :start_time, :step_name, :model
41
51
 
42
52
  def initialize(output: nil, prompt: TTY::Prompt.new)
@@ -78,10 +88,12 @@ module Aidp
78
88
  # Configure the provider with options
79
89
  # @param config [Hash] Configuration options, may include :model
80
90
  def configure(config)
81
- @model = config[:model] if config[:model]
91
+ if config[:model]
92
+ @model = resolve_model_name(config[:model].to_s)
93
+ end
82
94
  end
83
95
 
84
- def send_message(prompt:, session: nil)
96
+ def send_message(prompt:, session: nil, options: {})
85
97
  raise NotImplementedError, "#{self.class} must implement #send_message"
86
98
  end
87
99
 
@@ -482,11 +494,12 @@ module Aidp
482
494
  # Priority order:
483
495
  # 1. Quick mode (for testing)
484
496
  # 2. Provider-specific environment variable override
485
- # 3. Adaptive timeout based on step type
486
- # 4. Default timeout
497
+ # 3. Adaptive timeout based on step type and thinking tier
498
+ # 4. Default timeout (with tier multiplier)
487
499
  #
488
500
  # Override provider_env_var to customize the environment variable name
489
- def calculate_timeout
501
+ # @param options [Hash] Options hash that may include :tier
502
+ def calculate_timeout(options = {})
490
503
  if ENV["AIDP_QUICK_MODE"]
491
504
  display_message("⚡ Quick mode enabled - #{TIMEOUT_QUICK_MODE / 60} minute timeout", type: :highlight)
492
505
  return TIMEOUT_QUICK_MODE
@@ -495,47 +508,104 @@ module Aidp
495
508
  provider_env_var = "AIDP_#{name.upcase}_TIMEOUT"
496
509
  return ENV[provider_env_var].to_i if ENV[provider_env_var]
497
510
 
498
- step_timeout = adaptive_timeout
511
+ tier = options[:tier]&.to_s
512
+ step_timeout = adaptive_timeout(tier)
499
513
  if step_timeout
500
- display_message("🧠 Using adaptive timeout: #{step_timeout} seconds", type: :info)
514
+ tier_label = tier ? " (tier: #{tier})" : ""
515
+ display_message("🧠 Using adaptive timeout: #{step_timeout} seconds#{tier_label}", type: :info)
501
516
  return step_timeout
502
517
  end
503
518
 
504
- # Default timeout
505
- display_message("📋 Using default timeout: #{TIMEOUT_DEFAULT / 60} minutes", type: :info)
506
- TIMEOUT_DEFAULT
519
+ # Default timeout with tier multiplier
520
+ base_timeout = TIMEOUT_DEFAULT
521
+ final_timeout = apply_tier_multiplier(base_timeout, tier)
522
+ tier_label = tier ? " (tier: #{tier})" : ""
523
+ display_message("📋 Using default timeout: #{final_timeout / 60} minutes#{tier_label}", type: :info)
524
+ final_timeout
507
525
  end
508
526
 
509
- # Get adaptive timeout based on step type
527
+ # Get adaptive timeout based on step type and thinking tier
510
528
  #
511
529
  # This method returns different timeout values based on the type of operation
512
- # being performed, as indicated by the AIDP_CURRENT_STEP environment variable.
530
+ # being performed, as indicated by the AIDP_CURRENT_STEP environment variable,
531
+ # and applies a multiplier based on the thinking tier (mini/standard/thinking/pro/max).
513
532
  # Returns nil for unknown steps to allow calculate_timeout to use the default.
514
- def adaptive_timeout
515
- @adaptive_timeout ||= begin
516
- # Timeout recommendations based on step type patterns
517
- step_name = ENV["AIDP_CURRENT_STEP"] || ""
518
-
519
- case step_name
520
- when /REPOSITORY_ANALYSIS/
521
- TIMEOUT_REPOSITORY_ANALYSIS
522
- when /ARCHITECTURE_ANALYSIS/
523
- TIMEOUT_ARCHITECTURE_ANALYSIS
524
- when /TEST_ANALYSIS/
525
- TIMEOUT_TEST_ANALYSIS
526
- when /FUNCTIONALITY_ANALYSIS/
527
- TIMEOUT_FUNCTIONALITY_ANALYSIS
528
- when /DOCUMENTATION_ANALYSIS/
529
- TIMEOUT_DOCUMENTATION_ANALYSIS
530
- when /STATIC_ANALYSIS/
531
- TIMEOUT_STATIC_ANALYSIS
532
- when /REFACTORING_RECOMMENDATIONS/
533
- TIMEOUT_REFACTORING_RECOMMENDATIONS
534
- when /IMPLEMENTATION/
535
- TIMEOUT_IMPLEMENTATION
533
+ #
534
+ # @param tier [String, nil] The thinking tier (mini, standard, thinking, pro, max)
535
+ def adaptive_timeout(tier = nil)
536
+ # Don't cache - tier may change between calls
537
+ step_name = ENV["AIDP_CURRENT_STEP"] || ""
538
+
539
+ base_timeout = case step_name
540
+ when /REPOSITORY_ANALYSIS/
541
+ TIMEOUT_REPOSITORY_ANALYSIS
542
+ when /ARCHITECTURE_ANALYSIS/
543
+ TIMEOUT_ARCHITECTURE_ANALYSIS
544
+ when /TEST_ANALYSIS/
545
+ TIMEOUT_TEST_ANALYSIS
546
+ when /FUNCTIONALITY_ANALYSIS/
547
+ TIMEOUT_FUNCTIONALITY_ANALYSIS
548
+ when /DOCUMENTATION_ANALYSIS/
549
+ TIMEOUT_DOCUMENTATION_ANALYSIS
550
+ when /STATIC_ANALYSIS/
551
+ TIMEOUT_STATIC_ANALYSIS
552
+ when /REFACTORING_RECOMMENDATIONS/
553
+ TIMEOUT_REFACTORING_RECOMMENDATIONS
554
+ when /IMPLEMENTATION/
555
+ TIMEOUT_IMPLEMENTATION
556
+ else
557
+ nil # Return nil for unknown steps
558
+ end
559
+
560
+ return nil unless base_timeout
561
+
562
+ apply_tier_multiplier(base_timeout, tier)
563
+ end
564
+
565
+ # Apply tier-based multiplier to a base timeout
566
+ #
567
+ # @param base_timeout [Integer] The base timeout in seconds
568
+ # @param tier [String, nil] The thinking tier (mini, standard, thinking, pro, max)
569
+ # @return [Integer] The adjusted timeout with tier multiplier applied
570
+ def apply_tier_multiplier(base_timeout, tier)
571
+ return base_timeout unless tier
572
+
573
+ multiplier = TIER_TIMEOUT_MULTIPLIERS[tier.to_s] || 1.0
574
+ (base_timeout * multiplier).to_i
575
+ end
576
+
577
+ # Resolve a model name using the RubyLLM registry
578
+ #
579
+ # Attempts to resolve a model family name (e.g., "claude-3-5-haiku") to a
580
+ # versioned model name (e.g., "claude-3-5-haiku-20241022") using the RubyLLM
581
+ # registry. Falls back to using the name as-is if resolution fails.
582
+ #
583
+ # @param model_name [String] The model family name or versioned name
584
+ # @return [String] The resolved model name (versioned if found, original if not)
585
+ def resolve_model_name(model_name)
586
+ require_relative "../harness/ruby_llm_registry" unless defined?(Aidp::Harness::RubyLLMRegistry)
587
+
588
+ begin
589
+ registry = Aidp::Harness::RubyLLMRegistry.new
590
+ resolved = registry.resolve_model(model_name, provider: name)
591
+
592
+ if resolved
593
+ Aidp.log_debug(name, "Resolved model using registry",
594
+ requested: model_name,
595
+ resolved: resolved)
596
+ resolved
536
597
  else
537
- nil # Return nil for unknown steps
598
+ # Fall back to using the name as-is
599
+ Aidp.log_warn(name, "Model not found in registry, using as-is",
600
+ model: model_name)
601
+ model_name
538
602
  end
603
+ rescue => e
604
+ # If registry fails, fall back to using the name as-is
605
+ Aidp.log_error(name, "Registry lookup failed, using model name as-is",
606
+ model: model_name,
607
+ error: e.message)
608
+ model_name
539
609
  end
540
610
  end
541
611
 
@@ -76,7 +76,7 @@ module Aidp
76
76
  end
77
77
  end
78
78
 
79
- def send_message(prompt:, session: nil)
79
+ def send_message(prompt:, session: nil, options: {})
80
80
  raise "codex CLI not available" unless self.class.available?
81
81
 
82
82
  # Smart timeout calculation (store prompt length for adaptive logic)
@@ -117,7 +117,7 @@ module Aidp
117
117
  fetch_mcp_servers_cli || fetch_mcp_servers_config
118
118
  end
119
119
 
120
- def send_message(prompt:, session: nil)
120
+ def send_message(prompt:, session: nil, options: {})
121
121
  raise "cursor-agent not available" unless self.class.available?
122
122
 
123
123
  # Smart timeout calculation
@@ -76,7 +76,7 @@ module Aidp
76
76
  "Google Gemini"
77
77
  end
78
78
 
79
- def send_message(prompt:, session: nil)
79
+ def send_message(prompt:, session: nil, options: {})
80
80
  raise "gemini CLI not available" unless self.class.available?
81
81
 
82
82
  # Smart timeout calculation
@@ -51,7 +51,7 @@ module Aidp
51
51
  end
52
52
  end
53
53
 
54
- def send_message(prompt:, session: nil)
54
+ def send_message(prompt:, session: nil, options: {})
55
55
  raise "copilot CLI not available" unless self.class.available?
56
56
 
57
57
  # Smart timeout calculation