aidp 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -6
  3. data/lib/aidp/analyze/error_handler.rb +11 -0
  4. data/lib/aidp/cli/checkpoint_command.rb +198 -0
  5. data/lib/aidp/cli/config_command.rb +71 -0
  6. data/lib/aidp/cli/enhanced_input.rb +2 -0
  7. data/lib/aidp/cli/first_run_wizard.rb +8 -7
  8. data/lib/aidp/cli/harness_command.rb +102 -0
  9. data/lib/aidp/cli/jobs_command.rb +3 -3
  10. data/lib/aidp/cli/mcp_dashboard.rb +4 -3
  11. data/lib/aidp/cli/models_command.rb +662 -0
  12. data/lib/aidp/cli/providers_command.rb +223 -0
  13. data/lib/aidp/cli.rb +35 -456
  14. data/lib/aidp/daemon/runner.rb +2 -2
  15. data/lib/aidp/debug_mixin.rb +2 -9
  16. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  17. data/lib/aidp/execute/checkpoint_display.rb +38 -37
  18. data/lib/aidp/execute/interactive_repl.rb +2 -1
  19. data/lib/aidp/execute/prompt_manager.rb +4 -4
  20. data/lib/aidp/execute/work_loop_runner.rb +253 -56
  21. data/lib/aidp/execute/workflow_selector.rb +2 -2
  22. data/lib/aidp/harness/config_loader.rb +20 -11
  23. data/lib/aidp/harness/config_manager.rb +5 -5
  24. data/lib/aidp/harness/config_schema.rb +30 -8
  25. data/lib/aidp/harness/configuration.rb +105 -4
  26. data/lib/aidp/harness/enhanced_runner.rb +24 -15
  27. data/lib/aidp/harness/error_handler.rb +26 -5
  28. data/lib/aidp/harness/filter_strategy.rb +45 -0
  29. data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
  30. data/lib/aidp/harness/model_cache.rb +269 -0
  31. data/lib/aidp/harness/model_discovery_service.rb +259 -0
  32. data/lib/aidp/harness/model_registry.rb +201 -0
  33. data/lib/aidp/harness/output_filter.rb +136 -0
  34. data/lib/aidp/harness/provider_manager.rb +18 -3
  35. data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
  36. data/lib/aidp/harness/runner.rb +5 -0
  37. data/lib/aidp/harness/test_runner.rb +165 -27
  38. data/lib/aidp/harness/thinking_depth_manager.rb +223 -7
  39. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
  40. data/lib/aidp/logger.rb +35 -5
  41. data/lib/aidp/providers/adapter.rb +2 -4
  42. data/lib/aidp/providers/anthropic.rb +141 -128
  43. data/lib/aidp/providers/base.rb +98 -2
  44. data/lib/aidp/providers/capability_registry.rb +0 -1
  45. data/lib/aidp/providers/codex.rb +49 -67
  46. data/lib/aidp/providers/cursor.rb +71 -59
  47. data/lib/aidp/providers/gemini.rb +44 -60
  48. data/lib/aidp/providers/github_copilot.rb +2 -66
  49. data/lib/aidp/providers/kilocode.rb +24 -80
  50. data/lib/aidp/providers/opencode.rb +24 -80
  51. data/lib/aidp/safe_directory.rb +10 -3
  52. data/lib/aidp/setup/wizard.rb +345 -8
  53. data/lib/aidp/storage/csv_storage.rb +9 -3
  54. data/lib/aidp/storage/file_manager.rb +8 -2
  55. data/lib/aidp/storage/json_storage.rb +9 -3
  56. data/lib/aidp/version.rb +1 -1
  57. data/lib/aidp/watch/build_processor.rb +40 -1
  58. data/lib/aidp/watch/change_request_processor.rb +659 -0
  59. data/lib/aidp/watch/plan_generator.rb +93 -14
  60. data/lib/aidp/watch/plan_processor.rb +71 -8
  61. data/lib/aidp/watch/repository_client.rb +85 -20
  62. data/lib/aidp/watch/review_processor.rb +3 -3
  63. data/lib/aidp/watch/runner.rb +37 -0
  64. data/lib/aidp/watch/state_store.rb +46 -1
  65. data/lib/aidp/workflows/guided_agent.rb +3 -3
  66. data/lib/aidp/workstream_executor.rb +5 -2
  67. data/lib/aidp.rb +4 -0
  68. data/templates/aidp-development.yml.example +2 -2
  69. data/templates/aidp-production.yml.example +3 -3
  70. data/templates/aidp.yml.example +53 -0
  71. metadata +14 -1
data/lib/aidp/logger.rb CHANGED
@@ -18,6 +18,27 @@ module Aidp
18
18
  # Aidp.setup_logger(project_dir, config)
19
19
  # Aidp.logger.info("component", "message", key: "value")
20
20
  class Logger
21
+ # Custom log device that suppresses IOError exceptions for closed streams.
22
+ # This prevents "log shifting failed. closed stream" errors during test cleanup.
23
+ class SafeLogDevice < ::Logger::LogDevice
24
+ private
25
+
26
+ # Override handle_write_errors to suppress warnings for closed stream errors.
27
+ # Ruby's Logger::LogDevice calls warn() when write operations fail, which
28
+ # produces "log shifting failed. closed stream" messages during test cleanup.
29
+ def handle_write_errors(mesg)
30
+ yield
31
+ rescue *@reraise_write_errors
32
+ raise
33
+ rescue IOError => e
34
+ # Silently ignore closed stream errors - these are expected during
35
+ # test cleanup when loggers are finalized after streams are closed
36
+ return if e.message.include?("closed stream")
37
+ warn("log #{mesg} failed. #{e}")
38
+ rescue => e
39
+ warn("log #{mesg} failed. #{e}")
40
+ end
41
+ end
21
42
  LEVELS = {
22
43
  debug: ::Logger::DEBUG,
23
44
  info: ::Logger::INFO,
@@ -140,14 +161,17 @@ module Aidp
140
161
  dir = File.dirname(path)
141
162
  FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
142
163
 
143
- logger = ::Logger.new(path, @max_files, @max_size)
164
+ # Create logger with custom SafeLogDevice that suppresses closed stream errors
165
+ logdev = SafeLogDevice.new(path, shift_age: @max_files, shift_size: @max_size)
166
+ logger = ::Logger.new(logdev)
144
167
  logger.level = ::Logger::DEBUG # Control at write level instead
145
168
  logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
146
169
  logger
147
170
  rescue => e
148
171
  # Fall back to STDERR if file logging fails
149
172
  Kernel.warn "[AIDP Logger] Failed to create log file at #{path}: #{e.message}. Falling back to STDERR."
150
- logger = ::Logger.new($stderr)
173
+ logdev = SafeLogDevice.new($stderr)
174
+ logger = ::Logger.new(logdev)
151
175
  logger.level = ::Logger::DEBUG
152
176
  logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
153
177
  logger
@@ -246,7 +270,9 @@ module Aidp
246
270
  raw_input = dir.to_s
247
271
  raw_invalid = raw_input.empty? || raw_input.match?(/[<>|]/) || raw_input.match?(/[\x00-\x1F]/)
248
272
  if raw_invalid
249
- Kernel.warn "[AIDP Logger] Invalid project_dir '#{raw_input}' - falling back to #{Dir.pwd}"
273
+ unless ENV["RSPEC_RUNNING"] == "true"
274
+ Kernel.warn "[AIDP Logger] Invalid project_dir '#{raw_input}' - falling back to #{Dir.pwd}"
275
+ end
250
276
  if Dir.pwd == File::SEPARATOR
251
277
  fallback = begin
252
278
  home = Dir.home
@@ -255,7 +281,9 @@ module Aidp
255
281
  Dir.tmpdir
256
282
  end
257
283
  @root_fallback = fallback
258
- Kernel.warn "[AIDP Logger] Root directory detected - using #{fallback} for logging instead of '#{Dir.pwd}'"
284
+ unless ENV["RSPEC_RUNNING"] == "true"
285
+ Kernel.warn "[AIDP Logger] Root directory detected - using #{fallback} for logging instead of '#{Dir.pwd}'"
286
+ end
259
287
  return fallback
260
288
  end
261
289
  return Dir.pwd
@@ -268,7 +296,9 @@ module Aidp
268
296
  Dir.tmpdir
269
297
  end
270
298
  @root_fallback = fallback
271
- Kernel.warn "[AIDP Logger] Root directory detected - using #{fallback} for logging instead of '#{raw_input}'"
299
+ unless ENV["RSPEC_RUNNING"] == "true"
300
+ Kernel.warn "[AIDP Logger] Root directory detected - using #{fallback} for logging instead of '#{raw_input}'"
301
+ end
272
302
  return fallback
273
303
  end
274
304
  raw_input
@@ -65,8 +65,7 @@ module Aidp
65
65
  # supports_json_mode: true,
66
66
  # supports_tool_use: true,
67
67
  # supports_vision: false,
68
- # supports_file_upload: true,
69
- # streaming: true
68
+ # supports_file_upload: true
70
69
  # }
71
70
  def capabilities
72
71
  {
@@ -75,8 +74,7 @@ module Aidp
75
74
  supports_json_mode: false,
76
75
  supports_tool_use: false,
77
76
  supports_vision: false,
78
- supports_file_upload: false,
79
- streaming: false
77
+ supports_file_upload: false
80
78
  }
81
79
  end
82
80
 
@@ -9,10 +9,143 @@ module Aidp
9
9
  class Anthropic < Base
10
10
  include Aidp::DebugMixin
11
11
 
12
+ # Model name pattern for Anthropic Claude models
13
+ MODEL_PATTERN = /^claude-[\d.-]+-(?:opus|sonnet|haiku)(?:-\d{8})?$/i
14
+
12
15
  def self.available?
13
16
  !!Aidp::Util.which("claude")
14
17
  end
15
18
 
19
+ # Normalize a provider-specific model name to its model family
20
+ #
21
+ # Anthropic uses date-versioned models (e.g., "claude-3-5-sonnet-20241022").
22
+ # This method strips the date suffix to get the family name.
23
+ #
24
+ # @param provider_model_name [String] The versioned model name
25
+ # @return [String] The model family name (e.g., "claude-3-5-sonnet")
26
+ def self.model_family(provider_model_name)
27
+ # Strip date suffix: "claude-3-5-sonnet-20241022" → "claude-3-5-sonnet"
28
+ provider_model_name.sub(/-\d{8}$/, "")
29
+ end
30
+
31
+ # Convert a model family name to the provider's preferred model name
32
+ #
33
+ # Returns the family name as-is. Users can configure specific versions in aidp.yml.
34
+ #
35
+ # @param family_name [String] The model family name
36
+ # @return [String] The model name (same as family for flexibility)
37
+ def self.provider_model_name(family_name)
38
+ family_name
39
+ end
40
+
41
+ # Check if this provider supports a given model family
42
+ #
43
+ # @param family_name [String] The model family name
44
+ # @return [Boolean] True if it matches Claude model pattern
45
+ def self.supports_model_family?(family_name)
46
+ MODEL_PATTERN.match?(family_name)
47
+ end
48
+
49
+ # Discover available models from Claude CLI
50
+ #
51
+ # @return [Array<Hash>] Array of discovered models
52
+ def self.discover_models
53
+ return [] unless available?
54
+
55
+ begin
56
+ require "open3"
57
+ output, _, status = Open3.capture3("claude", "models", "list", {timeout: 10})
58
+ return [] unless status.success?
59
+
60
+ parse_models_list(output)
61
+ rescue => e
62
+ Aidp.log_debug("anthropic_provider", "discovery failed", error: e.message)
63
+ []
64
+ end
65
+ end
66
+
67
+ class << self
68
+ private
69
+
70
+ def parse_models_list(output)
71
+ return [] if output.nil? || output.empty?
72
+
73
+ models = []
74
+ lines = output.lines.map(&:strip)
75
+
76
+ # Skip header and separator lines
77
+ lines.reject! { |line| line.empty? || line.match?(/^[-=]+$/) || line.match?(/^(Model|Name)/i) }
78
+
79
+ lines.each do |line|
80
+ model_info = parse_model_line(line)
81
+ models << model_info if model_info
82
+ end
83
+
84
+ Aidp.log_info("anthropic_provider", "discovered models", count: models.size)
85
+ models
86
+ end
87
+
88
+ def parse_model_line(line)
89
+ # Format 1: Simple list of model names
90
+ if line.match?(/^claude-\d/)
91
+ model_name = line.split.first
92
+ return build_model_info(model_name)
93
+ end
94
+
95
+ # Format 2: Table format with columns
96
+ parts = line.split(/\s{2,}/)
97
+ if parts.size >= 1 && parts[0].match?(/^claude/)
98
+ model_name = parts[0]
99
+ model_name = "#{model_name}-#{parts[1]}" if parts.size > 1 && parts[1].match?(/^\d{8}$/)
100
+ return build_model_info(model_name)
101
+ end
102
+
103
+ # Format 3: JSON-like or key-value pairs
104
+ if line.match?(/name:\s*(.+)/)
105
+ model_name = $1.strip
106
+ return build_model_info(model_name)
107
+ end
108
+
109
+ nil
110
+ end
111
+
112
+ def build_model_info(model_name)
113
+ family = model_family(model_name)
114
+ tier = classify_tier(model_name)
115
+
116
+ {
117
+ name: model_name,
118
+ family: family,
119
+ tier: tier,
120
+ capabilities: extract_capabilities(model_name),
121
+ context_window: infer_context_window(family),
122
+ provider: "anthropic"
123
+ }
124
+ end
125
+
126
+ def classify_tier(model_name)
127
+ name_lower = model_name.downcase
128
+ return "advanced" if name_lower.include?("opus")
129
+ return "mini" if name_lower.include?("haiku")
130
+ return "standard" if name_lower.include?("sonnet")
131
+ "standard"
132
+ end
133
+
134
+ def extract_capabilities(model_name)
135
+ capabilities = ["chat", "code"]
136
+ name_lower = model_name.downcase
137
+ capabilities << "vision" unless name_lower.include?("haiku")
138
+ capabilities
139
+ end
140
+
141
+ def infer_context_window(family)
142
+ family.match?(/claude-3/) ? 200_000 : nil
143
+ end
144
+ end
145
+
146
+ # Public instance methods (called from workflows and harness)
147
+ public
148
+
16
149
  def name
17
150
  "anthropic"
18
151
  end
@@ -53,8 +186,7 @@ module Aidp
53
186
  supports_json_mode: true,
54
187
  supports_tool_use: true,
55
188
  supports_vision: false,
56
- supports_file_upload: true,
57
- streaming: true
189
+ supports_file_upload: true
58
190
  }
59
191
  end
60
192
 
@@ -115,19 +247,8 @@ module Aidp
115
247
  debug_provider("claude", "Starting execution", {timeout: timeout_seconds})
116
248
  debug_log("📝 Sending prompt to claude...", level: :info)
117
249
 
118
- # Check if streaming mode is enabled
119
- streaming_enabled = ENV["AIDP_STREAMING"] == "1" || ENV["DEBUG"] == "1"
120
-
121
- # Build command arguments with proper streaming support
122
- args = ["--print"]
123
- if streaming_enabled
124
- # Claude CLI requires --verbose when using --print with --output-format=stream-json
125
- args += ["--verbose", "--output-format=stream-json", "--include-partial-messages"]
126
- display_message("📺 True streaming enabled - real-time chunks from Claude API", type: :info)
127
- else
128
- # Use text format for non-streaming (default behavior)
129
- args += ["--output-format=text"]
130
- end
250
+ # Build command arguments
251
+ args = ["--print", "--output-format=text"]
131
252
 
132
253
  # Check if we should skip permissions (devcontainer support)
133
254
  if should_skip_permissions?
@@ -136,26 +257,20 @@ module Aidp
136
257
  end
137
258
 
138
259
  begin
139
- # Use debug_execute_command with streaming support
140
- result = debug_execute_command("claude", args: args, input: prompt, timeout: timeout_seconds, streaming: streaming_enabled)
260
+ result = debug_execute_command("claude", args: args, input: prompt, timeout: timeout_seconds)
141
261
 
142
262
  # Log the results
143
263
  debug_command("claude", args: args, input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
144
264
 
145
265
  if result.exit_status == 0
146
- # Handle different output formats
147
- if streaming_enabled && args.include?("--output-format=stream-json")
148
- # Parse stream-json output and extract final content
149
- parse_stream_json_output(result.out)
150
- else
151
- # Return text output as-is
152
- result.out
153
- end
266
+ result.out
154
267
  else
155
268
  # Detect auth issues in stdout/stderr (Claude sometimes prints JSON with auth error to stdout)
156
269
  combined = [result.out, result.err].compact.join("\n")
157
270
  if combined.downcase.include?("oauth token has expired") || combined.downcase.include?("authentication_error")
158
- error_message = "Authentication error from Claude CLI: token expired or invalid. Run 'claude /login' or refresh credentials."
271
+ error_message = "Authentication error from Claude CLI: token expired or invalid.\n" \
272
+ "Run 'claude /login' or refresh credentials.\n" \
273
+ "Note: Model discovery requires valid authentication."
159
274
  debug_error(StandardError.new(error_message), {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
160
275
  # Raise a recognizable error for classifier
161
276
  raise error_message
@@ -172,60 +287,6 @@ module Aidp
172
287
 
173
288
  private
174
289
 
175
- def calculate_timeout
176
- # Priority order for timeout calculation:
177
- # 1. Quick mode (for testing)
178
- # 2. Environment variable override
179
- # 3. Adaptive timeout based on step type
180
- # 4. Default timeout
181
-
182
- if ENV["AIDP_QUICK_MODE"]
183
- display_message("⚡ Quick mode enabled - #{TIMEOUT_QUICK_MODE / 60} minute timeout", type: :highlight)
184
- return TIMEOUT_QUICK_MODE
185
- end
186
-
187
- if ENV["AIDP_ANTHROPIC_TIMEOUT"]
188
- return ENV["AIDP_ANTHROPIC_TIMEOUT"].to_i
189
- end
190
-
191
- if adaptive_timeout
192
- display_message("🧠 Using adaptive timeout: #{adaptive_timeout} seconds", type: :info)
193
- return adaptive_timeout
194
- end
195
-
196
- # Default timeout
197
- display_message("📋 Using default timeout: #{TIMEOUT_DEFAULT / 60} minutes", type: :info)
198
- TIMEOUT_DEFAULT
199
- end
200
-
201
- def adaptive_timeout
202
- @adaptive_timeout ||= begin
203
- # Timeout recommendations based on step type patterns
204
- step_name = ENV["AIDP_CURRENT_STEP"] || ""
205
-
206
- case step_name
207
- when /REPOSITORY_ANALYSIS/
208
- TIMEOUT_REPOSITORY_ANALYSIS
209
- when /ARCHITECTURE_ANALYSIS/
210
- TIMEOUT_ARCHITECTURE_ANALYSIS
211
- when /TEST_ANALYSIS/
212
- TIMEOUT_TEST_ANALYSIS
213
- when /FUNCTIONALITY_ANALYSIS/
214
- TIMEOUT_FUNCTIONALITY_ANALYSIS
215
- when /DOCUMENTATION_ANALYSIS/
216
- TIMEOUT_DOCUMENTATION_ANALYSIS
217
- when /STATIC_ANALYSIS/
218
- TIMEOUT_STATIC_ANALYSIS
219
- when /REFACTORING_RECOMMENDATIONS/
220
- TIMEOUT_REFACTORING_RECOMMENDATIONS
221
- when /IMPLEMENTATION/
222
- TIMEOUT_IMPLEMENTATION
223
- else
224
- nil # Use default
225
- end
226
- end
227
- end
228
-
229
290
  # Check if we should skip permissions based on devcontainer configuration
230
291
  # Overrides base class to add logging and Claude-specific config check
231
292
  def should_skip_permissions?
@@ -243,54 +304,6 @@ module Aidp
243
304
  false
244
305
  end
245
306
 
246
- # Parse stream-json output from Claude CLI
247
- def parse_stream_json_output(output)
248
- return output if output.nil? || output.empty?
249
-
250
- # Stream-json output contains multiple JSON objects, one per line
251
- # We want to extract the final content from the last complete message
252
- lines = output.strip.split("\n")
253
- content_parts = []
254
-
255
- lines.each do |line|
256
- next if line.strip.empty?
257
-
258
- begin
259
- json_obj = JSON.parse(line)
260
-
261
- # Look for content in various possible structures
262
- if json_obj["type"] == "content_block_delta" && json_obj["delta"] && json_obj["delta"]["text"]
263
- content_parts << json_obj["delta"]["text"]
264
- elsif json_obj["content"]&.is_a?(Array)
265
- json_obj["content"].each do |content_item|
266
- content_parts << content_item["text"] if content_item["text"]
267
- end
268
- elsif json_obj["message"] && json_obj["message"]["content"]
269
- if json_obj["message"]["content"].is_a?(Array)
270
- json_obj["message"]["content"].each do |content_item|
271
- content_parts << content_item["text"] if content_item["text"]
272
- end
273
- elsif json_obj["message"]["content"].is_a?(String)
274
- content_parts << json_obj["message"]["content"]
275
- end
276
- end
277
- rescue JSON::ParserError => e
278
- debug_log("⚠️ Failed to parse JSON line: #{e.message}", level: :warn, data: {line: line})
279
- # If JSON parsing fails, treat as plain text
280
- content_parts << line
281
- end
282
- end
283
-
284
- result = content_parts.join
285
-
286
- # Fallback: if no content found in JSON, return original output
287
- result.empty? ? output : result
288
- rescue => e
289
- debug_log("⚠️ Failed to parse stream-json output: #{e.message}", level: :warn)
290
- # Return original output if parsing fails
291
- output
292
- end
293
-
294
307
  # Parse Claude MCP server list output
295
308
  def parse_claude_mcp_output(output)
296
309
  servers = []
@@ -324,6 +324,42 @@ module Aidp
324
324
  end
325
325
  end
326
326
 
327
+ # Helper method for registry-based model discovery
328
+ #
329
+ # Providers that use the model registry can call this method to discover models
330
+ # based on a model family pattern.
331
+ #
332
+ # @param model_pattern [Regexp] Pattern to match model families
333
+ # @param provider_name [String] Name of the provider
334
+ # @return [Array<Hash>] Array of discovered models
335
+ def self.discover_models_from_registry(model_pattern, provider_name)
336
+ require_relative "../harness/model_registry"
337
+ registry = Aidp::Harness::ModelRegistry.new
338
+
339
+ # Get all models from registry that match the pattern
340
+ models = registry.all_families.filter_map do |family|
341
+ next unless model_pattern.match?(family)
342
+
343
+ info = registry.get_model_info(family)
344
+ next unless info
345
+
346
+ {
347
+ name: family,
348
+ family: family,
349
+ tier: info["tier"],
350
+ capabilities: info["capabilities"] || [],
351
+ context_window: info["context_window"],
352
+ provider: provider_name
353
+ }
354
+ end
355
+
356
+ Aidp.log_info("#{provider_name}_provider", "using registry models", count: models.size)
357
+ models
358
+ rescue => e
359
+ Aidp.log_debug("#{provider_name}_provider", "discovery failed", error: e.message)
360
+ []
361
+ end
362
+
327
363
  protected
328
364
 
329
365
  # Log message to job if in background mode
@@ -371,8 +407,6 @@ module Aidp
371
407
  (success_rate * 50) + ((1 - rate_limit_ratio) * 30) + (response_time_score * 0.2)
372
408
  end
373
409
 
374
- protected
375
-
376
410
  # Update spinner status with elapsed time
377
411
  # This is a shared method used by all providers to display progress
378
412
  def update_spinner_status(spinner, elapsed, provider_name)
@@ -419,6 +453,68 @@ module Aidp
419
453
  false # Base implementation returns false, subclasses should override
420
454
  end
421
455
 
456
+ # Calculate timeout for provider operations
457
+ #
458
+ # Priority order:
459
+ # 1. Quick mode (for testing)
460
+ # 2. Provider-specific environment variable override
461
+ # 3. Adaptive timeout based on step type
462
+ # 4. Default timeout
463
+ #
464
+ # Override provider_env_var to customize the environment variable name
465
+ def calculate_timeout
466
+ if ENV["AIDP_QUICK_MODE"]
467
+ display_message("⚡ Quick mode enabled - #{TIMEOUT_QUICK_MODE / 60} minute timeout", type: :highlight)
468
+ return TIMEOUT_QUICK_MODE
469
+ end
470
+
471
+ provider_env_var = "AIDP_#{name.upcase}_TIMEOUT"
472
+ return ENV[provider_env_var].to_i if ENV[provider_env_var]
473
+
474
+ step_timeout = adaptive_timeout
475
+ if step_timeout
476
+ display_message("🧠 Using adaptive timeout: #{step_timeout} seconds", type: :info)
477
+ return step_timeout
478
+ end
479
+
480
+ # Default timeout
481
+ display_message("📋 Using default timeout: #{TIMEOUT_DEFAULT / 60} minutes", type: :info)
482
+ TIMEOUT_DEFAULT
483
+ end
484
+
485
+ # Get adaptive timeout based on step type
486
+ #
487
+ # This method returns different timeout values based on the type of operation
488
+ # being performed, as indicated by the AIDP_CURRENT_STEP environment variable.
489
+ # Returns nil for unknown steps to allow calculate_timeout to use the default.
490
+ def adaptive_timeout
491
+ @adaptive_timeout ||= begin
492
+ # Timeout recommendations based on step type patterns
493
+ step_name = ENV["AIDP_CURRENT_STEP"] || ""
494
+
495
+ case step_name
496
+ when /REPOSITORY_ANALYSIS/
497
+ TIMEOUT_REPOSITORY_ANALYSIS
498
+ when /ARCHITECTURE_ANALYSIS/
499
+ TIMEOUT_ARCHITECTURE_ANALYSIS
500
+ when /TEST_ANALYSIS/
501
+ TIMEOUT_TEST_ANALYSIS
502
+ when /FUNCTIONALITY_ANALYSIS/
503
+ TIMEOUT_FUNCTIONALITY_ANALYSIS
504
+ when /DOCUMENTATION_ANALYSIS/
505
+ TIMEOUT_DOCUMENTATION_ANALYSIS
506
+ when /STATIC_ANALYSIS/
507
+ TIMEOUT_STATIC_ANALYSIS
508
+ when /REFACTORING_RECOMMENDATIONS/
509
+ TIMEOUT_REFACTORING_RECOMMENDATIONS
510
+ when /IMPLEMENTATION/
511
+ TIMEOUT_IMPLEMENTATION
512
+ else
513
+ nil # Return nil for unknown steps
514
+ end
515
+ end
516
+ end
517
+
422
518
  private
423
519
  end
424
520
  end
@@ -16,7 +16,6 @@ module Aidp
16
16
  :supports_tool_use, # Boolean: supports tool/function calling
17
17
  :supports_vision, # Boolean: supports image/vision inputs
18
18
  :supports_file_upload, # Boolean: supports file uploads
19
- :streaming, # Boolean: supports streaming responses
20
19
  :supports_mcp, # Boolean: supports Model Context Protocol
21
20
  :max_tokens, # Maximum tokens per response
22
21
  :supports_dangerous_mode # Boolean: supports elevated permissions mode