aidp 0.26.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aidp/cli/checkpoint_command.rb +198 -0
  3. data/lib/aidp/cli/config_command.rb +71 -0
  4. data/lib/aidp/cli/enhanced_input.rb +2 -0
  5. data/lib/aidp/cli/first_run_wizard.rb +8 -7
  6. data/lib/aidp/cli/harness_command.rb +102 -0
  7. data/lib/aidp/cli/jobs_command.rb +3 -3
  8. data/lib/aidp/cli/mcp_dashboard.rb +4 -3
  9. data/lib/aidp/cli/models_command.rb +662 -0
  10. data/lib/aidp/cli/providers_command.rb +223 -0
  11. data/lib/aidp/cli.rb +35 -456
  12. data/lib/aidp/daemon/runner.rb +2 -2
  13. data/lib/aidp/debug_mixin.rb +2 -9
  14. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  15. data/lib/aidp/execute/checkpoint_display.rb +38 -37
  16. data/lib/aidp/execute/interactive_repl.rb +2 -1
  17. data/lib/aidp/execute/prompt_manager.rb +4 -4
  18. data/lib/aidp/execute/work_loop_runner.rb +29 -2
  19. data/lib/aidp/execute/workflow_selector.rb +2 -2
  20. data/lib/aidp/harness/config_manager.rb +5 -5
  21. data/lib/aidp/harness/configuration.rb +32 -2
  22. data/lib/aidp/harness/enhanced_runner.rb +24 -15
  23. data/lib/aidp/harness/error_handler.rb +26 -5
  24. data/lib/aidp/harness/model_cache.rb +269 -0
  25. data/lib/aidp/harness/model_discovery_service.rb +259 -0
  26. data/lib/aidp/harness/model_registry.rb +201 -0
  27. data/lib/aidp/harness/runner.rb +5 -0
  28. data/lib/aidp/harness/thinking_depth_manager.rb +223 -7
  29. data/lib/aidp/message_display.rb +0 -46
  30. data/lib/aidp/providers/adapter.rb +2 -4
  31. data/lib/aidp/providers/anthropic.rb +141 -128
  32. data/lib/aidp/providers/base.rb +98 -2
  33. data/lib/aidp/providers/capability_registry.rb +0 -1
  34. data/lib/aidp/providers/codex.rb +49 -67
  35. data/lib/aidp/providers/cursor.rb +71 -59
  36. data/lib/aidp/providers/gemini.rb +44 -60
  37. data/lib/aidp/providers/github_copilot.rb +2 -66
  38. data/lib/aidp/providers/kilocode.rb +24 -80
  39. data/lib/aidp/providers/opencode.rb +24 -80
  40. data/lib/aidp/setup/wizard.rb +345 -8
  41. data/lib/aidp/version.rb +1 -1
  42. data/lib/aidp/watch/plan_generator.rb +93 -14
  43. data/lib/aidp/watch/review_processor.rb +3 -3
  44. data/lib/aidp/workflows/guided_agent.rb +3 -3
  45. data/templates/aidp-development.yml.example +2 -2
  46. data/templates/aidp-production.yml.example +3 -3
  47. metadata +9 -1
@@ -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
@@ -10,10 +10,35 @@ module Aidp
10
10
  class Codex < Base
11
11
  include Aidp::DebugMixin
12
12
 
13
+ # Model name pattern for OpenAI models (since Codex uses OpenAI)
14
+ MODEL_PATTERN = /^gpt-[\d.o-]+(?:-turbo)?(?:-mini)?$/i
15
+ LONG_PROMPT_THRESHOLD = 8000
16
+ LONG_PROMPT_TIMEOUT = 900 # 15 minutes for large prompts
17
+
13
18
  def self.available?
14
19
  !!Aidp::Util.which("codex")
15
20
  end
16
21
 
22
+ # Check if this provider supports a given model family
23
+ #
24
+ # @param family_name [String] The model family name
25
+ # @return [Boolean] True if it matches OpenAI model pattern
26
+ def self.supports_model_family?(family_name)
27
+ MODEL_PATTERN.match?(family_name)
28
+ end
29
+
30
+ # Discover available models from registry
31
+ #
32
+ # Note: Codex CLI doesn't have a standard model listing command
33
+ # Returns registry-based models that match OpenAI patterns
34
+ #
35
+ # @return [Array<Hash>] Array of discovered models
36
+ def self.discover_models
37
+ return [] unless available?
38
+
39
+ discover_models_from_registry(MODEL_PATTERN, "codex")
40
+ end
41
+
17
42
  def name
18
43
  "codex"
19
44
  end
@@ -37,18 +62,13 @@ module Aidp
37
62
  def send_message(prompt:, session: nil)
38
63
  raise "codex CLI not available" unless self.class.available?
39
64
 
40
- # Smart timeout calculation
65
+ # Smart timeout calculation (store prompt length for adaptive logic)
66
+ @current_codex_prompt_length = prompt.length
41
67
  timeout_seconds = calculate_timeout
42
68
 
43
69
  debug_provider("codex", "Starting execution", {timeout: timeout_seconds})
44
70
  debug_log("šŸ“ Sending prompt to codex (length: #{prompt.length})", level: :info)
45
71
 
46
- # Check if streaming mode is enabled
47
- streaming_enabled = ENV["AIDP_STREAMING"] == "1" || ENV["DEBUG"] == "1"
48
- if streaming_enabled
49
- display_message("šŸ“ŗ Streaming mode enabled - output will appear in real-time", type: :info)
50
- end
51
-
52
72
  # Set up activity monitoring
53
73
  setup_activity_monitoring("codex", method(:activity_callback))
54
74
  record_activity("Starting codex execution")
@@ -94,7 +114,7 @@ module Aidp
94
114
  end
95
115
 
96
116
  # Use debug_execute_command for better debugging
97
- result = debug_execute_command("codex", args: args, timeout: timeout_seconds, streaming: streaming_enabled)
117
+ result = debug_execute_command("codex", args: args, timeout: timeout_seconds)
98
118
 
99
119
  # Log the results
100
120
  debug_command("codex", args: args, input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
@@ -116,6 +136,7 @@ module Aidp
116
136
  raise
117
137
  ensure
118
138
  cleanup_activity_display(activity_display_thread, spinner)
139
+ @current_codex_prompt_length = nil
119
140
  end
120
141
  end
121
142
 
@@ -161,22 +182,17 @@ module Aidp
161
182
 
162
183
  # Internal helper for send_with_options - executes with custom arguments
163
184
  def send_with_custom_args(prompt:, args:)
185
+ @current_codex_prompt_length = prompt.length
164
186
  timeout_seconds = calculate_timeout
165
187
 
166
188
  debug_provider("codex", "Starting execution", {timeout: timeout_seconds, args: args})
167
189
  debug_log("šŸ“ Sending prompt to codex with custom args", level: :info)
168
190
 
169
- # Check if streaming mode is enabled
170
- streaming_enabled = ENV["AIDP_STREAMING"] == "1" || ENV["DEBUG"] == "1"
171
- if streaming_enabled
172
- display_message("šŸ“ŗ Display streaming enabled - output buffering reduced (codex CLI does not support true streaming)", type: :info)
173
- end
174
-
175
191
  setup_activity_monitoring("codex", method(:activity_callback))
176
192
  record_activity("Starting codex execution with custom args")
177
193
 
178
194
  begin
179
- result = debug_execute_command("codex", args: args, timeout: timeout_seconds, streaming: streaming_enabled)
195
+ result = debug_execute_command("codex", args: args, timeout: timeout_seconds)
180
196
  debug_command("codex", args: args, output: result.out, error: result.err, exit_code: result.exit_status)
181
197
 
182
198
  if result.exit_status == 0
@@ -191,58 +207,8 @@ module Aidp
191
207
  mark_failed("codex execution failed: #{e.message}")
192
208
  debug_error(e, {provider: "codex", prompt_length: prompt.length})
193
209
  raise
194
- end
195
- end
196
-
197
- def calculate_timeout
198
- # Priority order for timeout calculation:
199
- # 1. Quick mode (for testing)
200
- # 2. Environment variable override
201
- # 3. Adaptive timeout based on step type
202
- # 4. Default timeout
203
-
204
- if ENV["AIDP_QUICK_MODE"]
205
- display_message("⚔ Quick mode enabled - #{TIMEOUT_QUICK_MODE / 60} minute timeout", type: :highlight)
206
- return TIMEOUT_QUICK_MODE
207
- end
208
-
209
- if ENV["AIDP_CODEX_TIMEOUT"]
210
- return ENV["AIDP_CODEX_TIMEOUT"].to_i
211
- end
212
-
213
- if adaptive_timeout
214
- display_message("🧠 Using adaptive timeout: #{adaptive_timeout} seconds", type: :info)
215
- return adaptive_timeout
216
- end
217
-
218
- # Default timeout
219
- display_message("šŸ“‹ Using default timeout: #{TIMEOUT_DEFAULT / 60} minutes", type: :info)
220
- TIMEOUT_DEFAULT
221
- end
222
-
223
- def adaptive_timeout
224
- @adaptive_timeout ||= begin
225
- # Timeout recommendations based on step type patterns
226
- step_name = ENV["AIDP_CURRENT_STEP"] || ""
227
-
228
- case step_name
229
- when /REPOSITORY_ANALYSIS/
230
- TIMEOUT_REPOSITORY_ANALYSIS
231
- when /ARCHITECTURE_ANALYSIS/
232
- TIMEOUT_ARCHITECTURE_ANALYSIS
233
- when /TEST_ANALYSIS/
234
- TIMEOUT_TEST_ANALYSIS
235
- when /FUNCTIONALITY_ANALYSIS/
236
- TIMEOUT_FUNCTIONALITY_ANALYSIS
237
- when /DOCUMENTATION_ANALYSIS/
238
- TIMEOUT_DOCUMENTATION_ANALYSIS
239
- when /STATIC_ANALYSIS/
240
- TIMEOUT_STATIC_ANALYSIS
241
- when /REFACTORING_RECOMMENDATIONS/
242
- TIMEOUT_REFACTORING_RECOMMENDATIONS
243
- else
244
- nil # Use default
245
- end
210
+ ensure
211
+ @current_codex_prompt_length = nil
246
212
  end
247
213
  end
248
214
 
@@ -257,6 +223,22 @@ module Aidp
257
223
  display_message("\nāŒ Codex CLI failed: #{message}", type: :error)
258
224
  end
259
225
  end
226
+
227
+ def calculate_timeout
228
+ env_override = ENV["AIDP_CODEX_TIMEOUT"]
229
+ return env_override.to_i if env_override&.match?(/^\d+$/)
230
+
231
+ base_timeout = super
232
+
233
+ prompt_length = @current_codex_prompt_length
234
+ return base_timeout unless prompt_length && prompt_length >= LONG_PROMPT_THRESHOLD
235
+
236
+ extended_timeout = [base_timeout, LONG_PROMPT_TIMEOUT].max
237
+ if extended_timeout > base_timeout
238
+ display_message("ā±ļø Codex prompt length #{prompt_length} detected - extending timeout to #{extended_timeout} seconds", type: :info)
239
+ end
240
+ extended_timeout
241
+ end
260
242
  end
261
243
  end
262
244
  end