aidp 0.22.0 → 0.24.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +145 -31
  3. data/lib/aidp/cli.rb +19 -2
  4. data/lib/aidp/execute/work_loop_runner.rb +252 -45
  5. data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
  6. data/lib/aidp/harness/condition_detector.rb +42 -8
  7. data/lib/aidp/harness/config_manager.rb +7 -0
  8. data/lib/aidp/harness/config_schema.rb +25 -0
  9. data/lib/aidp/harness/configuration.rb +69 -6
  10. data/lib/aidp/harness/error_handler.rb +117 -44
  11. data/lib/aidp/harness/provider_manager.rb +64 -0
  12. data/lib/aidp/harness/provider_metrics.rb +138 -0
  13. data/lib/aidp/harness/runner.rb +110 -35
  14. data/lib/aidp/harness/simple_user_interface.rb +4 -0
  15. data/lib/aidp/harness/state/ui_state.rb +0 -10
  16. data/lib/aidp/harness/state_manager.rb +1 -15
  17. data/lib/aidp/harness/test_runner.rb +39 -2
  18. data/lib/aidp/logger.rb +34 -4
  19. data/lib/aidp/providers/adapter.rb +241 -0
  20. data/lib/aidp/providers/anthropic.rb +75 -7
  21. data/lib/aidp/providers/base.rb +29 -1
  22. data/lib/aidp/providers/capability_registry.rb +205 -0
  23. data/lib/aidp/providers/codex.rb +14 -0
  24. data/lib/aidp/providers/error_taxonomy.rb +195 -0
  25. data/lib/aidp/providers/gemini.rb +3 -2
  26. data/lib/aidp/setup/devcontainer/backup_manager.rb +11 -4
  27. data/lib/aidp/setup/provider_registry.rb +107 -0
  28. data/lib/aidp/setup/wizard.rb +189 -31
  29. data/lib/aidp/version.rb +1 -1
  30. data/lib/aidp/watch/build_processor.rb +357 -27
  31. data/lib/aidp/watch/plan_generator.rb +16 -1
  32. data/lib/aidp/watch/plan_processor.rb +54 -3
  33. data/lib/aidp/watch/repository_client.rb +78 -4
  34. data/lib/aidp/watch/repository_safety_checker.rb +12 -3
  35. data/lib/aidp/watch/runner.rb +52 -10
  36. data/lib/aidp/workflows/guided_agent.rb +53 -0
  37. data/lib/aidp/worktree.rb +67 -10
  38. data/templates/work_loop/decide_whats_next.md +21 -0
  39. data/templates/work_loop/diagnose_failures.md +21 -0
  40. metadata +10 -3
  41. /data/{bin → exe}/aidp +0 -0
data/lib/aidp/logger.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "logger"
4
4
  require "json"
5
5
  require "fileutils"
6
+ require "pathname"
6
7
 
7
8
  module Aidp
8
9
  # Unified structured logger for all AIDP operations
@@ -84,12 +85,30 @@ module Aidp
84
85
  private
85
86
 
86
87
  def determine_log_level
87
- # Priority: ENV > config > default
88
- level_str = ENV["AIDP_LOG_LEVEL"] || @config[:level] || "info"
89
- level_sym = level_str.to_sym
88
+ # Priority: explicit env override > DEBUG flags > config > default
89
+ level_str = if ENV["AIDP_LOG_LEVEL"]
90
+ ENV["AIDP_LOG_LEVEL"]
91
+ elsif debug_env_enabled?
92
+ "debug"
93
+ elsif @config[:level]
94
+ @config[:level]
95
+ else
96
+ "info"
97
+ end
98
+ level_sym = level_str.to_s.to_sym
90
99
  LEVELS.key?(level_sym) ? level_sym : :info
91
100
  end
92
101
 
102
+ def debug_env_enabled?
103
+ raw = ENV["AIDP_DEBUG"] || ENV["DEBUG"]
104
+ return false if raw.nil?
105
+
106
+ normalized = raw.to_s.strip.downcase
107
+ return true if %w[true on yes debug].include?(normalized)
108
+
109
+ /\A\d+\z/.match?(normalized) ? normalized.to_i.positive? : false
110
+ end
111
+
93
112
  def should_log?(level)
94
113
  LEVELS[level] >= LEVELS[@level]
95
114
  end
@@ -106,7 +125,7 @@ module Aidp
106
125
  end
107
126
 
108
127
  def setup_logger
109
- info_path = File.join(@project_dir, INFO_LOG)
128
+ info_path = determine_log_file_path
110
129
  @logger = create_logger(info_path)
111
130
  # Emit instrumentation after logger is available (avoid recursive Aidp.log_* calls during bootstrap)
112
131
  return unless @instrument_internal
@@ -181,6 +200,17 @@ module Aidp
181
200
  JSON.generate(entry)
182
201
  end
183
202
 
203
+ def determine_log_file_path
204
+ custom = (ENV["AIDP_LOG_FILE"] || @config[:file]).to_s.strip
205
+ relative_path = custom.empty? ? INFO_LOG : custom
206
+
207
+ if Pathname.new(relative_path).absolute?
208
+ relative_path
209
+ else
210
+ File.join(@project_dir, relative_path)
211
+ end
212
+ end
213
+
184
214
  # Redaction patterns for common secrets
185
215
  REDACTION_PATTERNS = [
186
216
  # API keys and tokens (with capture groups)
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Providers
5
+ # ProviderAdapter defines the standardized interface that all provider implementations
6
+ # must conform to. This ensures consistent behavior across different AI model providers
7
+ # while allowing for provider-specific implementations.
8
+ #
9
+ # Design Philosophy:
10
+ # - Adapters are stateless; delegate throttling, retries, and escalation to coordinator
11
+ # - Store provider-specific regex matchers adjacent to adapters for maintainability
12
+ # - Single semantic flags map to provider-specific equivalents
13
+ #
14
+ # @see https://github.com/viamin/aidp/issues/243
15
+ module Adapter
16
+ # Core interface methods that all providers must implement
17
+
18
+ # Provider identifier (e.g., "anthropic", "cursor", "gemini")
19
+ # @return [String] unique lowercase identifier for this provider
20
+ def name
21
+ raise NotImplementedError, "#{self.class} must implement #name"
22
+ end
23
+
24
+ # Human-friendly display name for UI
25
+ # @return [String] display name (e.g., "Anthropic Claude", "Cursor AI")
26
+ def display_name
27
+ name
28
+ end
29
+
30
+ # Send a message to the provider and get a response
31
+ # @param prompt [String] the prompt to send
32
+ # @param session [String, nil] optional session identifier for context
33
+ # @param options [Hash] additional options for the request
34
+ # @return [Hash, String] provider response
35
+ def send_message(prompt:, session: nil, **options)
36
+ raise NotImplementedError, "#{self.class} must implement #send_message"
37
+ end
38
+
39
+ # Capability declaration methods
40
+
41
+ # Check if the provider supports Model Context Protocol
42
+ # @return [Boolean] true if MCP is supported
43
+ def supports_mcp?
44
+ false
45
+ end
46
+
47
+ # Fetch MCP servers configured for this provider
48
+ # @return [Array<Hash>] array of MCP server configurations
49
+ def fetch_mcp_servers
50
+ []
51
+ end
52
+
53
+ # Check if the provider is available on this system
54
+ # @return [Boolean] true if provider CLI or API is accessible
55
+ def available?
56
+ true
57
+ end
58
+
59
+ # Declare provider capabilities
60
+ # @return [Hash] capabilities hash with feature flags
61
+ # @example
62
+ # {
63
+ # reasoning_tiers: ["mini", "standard", "thinking"],
64
+ # context_window: 200_000,
65
+ # supports_json_mode: true,
66
+ # supports_tool_use: true,
67
+ # supports_vision: false,
68
+ # supports_file_upload: true,
69
+ # streaming: true
70
+ # }
71
+ def capabilities
72
+ {
73
+ reasoning_tiers: [],
74
+ context_window: 100_000,
75
+ supports_json_mode: false,
76
+ supports_tool_use: false,
77
+ supports_vision: false,
78
+ supports_file_upload: false,
79
+ streaming: false
80
+ }
81
+ end
82
+
83
+ # Dangerous permissions abstraction
84
+
85
+ # Check if the provider supports dangerous/elevated permissions mode
86
+ # @return [Boolean] true if dangerous mode is supported
87
+ def supports_dangerous_mode?
88
+ false
89
+ end
90
+
91
+ # Get the provider-specific flag(s) for enabling dangerous mode
92
+ # Maps the semantic `dangerous: true` flag to provider-specific equivalents
93
+ # @return [Array<String>] provider-specific CLI flags
94
+ # @example Anthropic
95
+ # ["--dangerously-skip-permissions"]
96
+ # @example Gemini (hypothetical)
97
+ # ["--yolo"]
98
+ def dangerous_mode_flags
99
+ []
100
+ end
101
+
102
+ # Check if dangerous mode is currently enabled
103
+ # @return [Boolean] true if dangerous mode is active
104
+ def dangerous_mode_enabled?
105
+ @dangerous_mode_enabled ||= false
106
+ end
107
+
108
+ # Enable or disable dangerous mode
109
+ # @param enabled [Boolean] whether to enable dangerous mode
110
+ # @return [void]
111
+ def dangerous_mode=(enabled)
112
+ @dangerous_mode_enabled = enabled
113
+ end
114
+
115
+ # Error classification and handling
116
+
117
+ # Get error classification regex patterns for this provider
118
+ # @return [Hash<Symbol, Array<Regexp>>] mapping of error categories to regex patterns
119
+ # @example
120
+ # {
121
+ # rate_limited: [/rate.?limit/i, /quota.*exceeded/i],
122
+ # auth_expired: [/authentication.*failed/i, /invalid.*api.*key/i],
123
+ # quota_exceeded: [/quota.*exceeded/i, /usage.*limit/i],
124
+ # transient: [/timeout/i, /connection.*reset/i, /temporary.*error/i],
125
+ # permanent: [/invalid.*model/i, /unsupported.*operation/i]
126
+ # }
127
+ def error_patterns
128
+ {}
129
+ end
130
+
131
+ # Classify an error into the standardized error taxonomy
132
+ # @param error [StandardError] the error to classify
133
+ # @return [Symbol] error category (:rate_limited, :auth_expired, :quota_exceeded, :transient, :permanent)
134
+ def classify_error(error)
135
+ message = error.message.to_s
136
+
137
+ # First check provider-specific patterns
138
+ error_patterns.each do |category, patterns|
139
+ patterns.each do |pattern|
140
+ return category if message.match?(pattern)
141
+ end
142
+ end
143
+
144
+ # Fall back to ErrorTaxonomy for classification
145
+ require_relative "error_taxonomy"
146
+ Aidp::Providers::ErrorTaxonomy.classify_message(message)
147
+ end
148
+
149
+ # Get normalized error metadata
150
+ # @param error [StandardError] the error to process
151
+ # @return [Hash] normalized error information
152
+ def error_metadata(error)
153
+ {
154
+ provider: name,
155
+ error_category: classify_error(error),
156
+ error_class: error.class.name,
157
+ message: redact_secrets(error.message),
158
+ timestamp: Time.now.iso8601,
159
+ retryable: retryable_error?(error)
160
+ }
161
+ end
162
+
163
+ # Check if an error is retryable
164
+ # @param error [StandardError] the error to check
165
+ # @return [Boolean] true if the error should be retried
166
+ def retryable_error?(error)
167
+ category = classify_error(error)
168
+ [:transient].include?(category)
169
+ end
170
+
171
+ # Logging and metrics
172
+
173
+ # Get logging metadata for this provider
174
+ # @return [Hash] metadata for structured logging
175
+ def logging_metadata
176
+ {
177
+ provider: name,
178
+ display_name: display_name,
179
+ supports_mcp: supports_mcp?,
180
+ available: available?,
181
+ dangerous_mode: dangerous_mode_enabled?
182
+ }
183
+ end
184
+
185
+ # Redact secrets from log messages
186
+ # @param message [String] message potentially containing secrets
187
+ # @return [String] message with secrets redacted
188
+ def redact_secrets(message)
189
+ # Redact common secret patterns
190
+ message = message.gsub(/api[_-]?key[:\s=]+[^\s&]+/i, "api_key=[REDACTED]")
191
+ message = message.gsub(/token[:\s=]+[^\s&]+/i, "token=[REDACTED]")
192
+ message = message.gsub(/password[:\s=]+[^\s&]+/i, "password=[REDACTED]")
193
+ message = message.gsub(/bearer\s+[^\s&]+/i, "bearer [REDACTED]")
194
+ message.gsub(/sk-[a-zA-Z0-9_-]{20,}/i, "sk-[REDACTED]")
195
+ end
196
+
197
+ # Configuration validation
198
+
199
+ # Validate provider configuration
200
+ # @param config [Hash] configuration to validate
201
+ # @return [Hash] validation result with :valid, :errors, :warnings keys
202
+ def validate_config(config)
203
+ errors = []
204
+ warnings = []
205
+
206
+ # Validate required fields
207
+ unless config[:type]
208
+ errors << "Provider type is required"
209
+ end
210
+
211
+ unless ["usage_based", "subscription", "passthrough"].include?(config[:type])
212
+ errors << "Provider type must be one of: usage_based, subscription, passthrough"
213
+ end
214
+
215
+ # Validate models if present
216
+ if config[:models] && !config[:models].is_a?(Array)
217
+ errors << "Models must be an array"
218
+ end
219
+
220
+ {
221
+ valid: errors.empty?,
222
+ errors: errors,
223
+ warnings: warnings
224
+ }
225
+ end
226
+
227
+ # Provider health and status
228
+
229
+ # Check provider health
230
+ # @return [Hash] health status information
231
+ def health_status
232
+ {
233
+ provider: name,
234
+ available: available?,
235
+ healthy: available?,
236
+ timestamp: Time.now.iso8601
237
+ }
238
+ end
239
+ end
240
+ end
241
+ end
@@ -44,6 +44,68 @@ module Aidp
44
44
  self.class.available?
45
45
  end
46
46
 
47
+ # ProviderAdapter interface methods
48
+
49
+ def capabilities
50
+ {
51
+ reasoning_tiers: ["mini", "standard", "thinking"],
52
+ context_window: 200_000,
53
+ supports_json_mode: true,
54
+ supports_tool_use: true,
55
+ supports_vision: false,
56
+ supports_file_upload: true,
57
+ streaming: true
58
+ }
59
+ end
60
+
61
+ def supports_dangerous_mode?
62
+ true
63
+ end
64
+
65
+ def dangerous_mode_flags
66
+ ["--dangerously-skip-permissions"]
67
+ end
68
+
69
+ def error_patterns
70
+ {
71
+ rate_limited: [
72
+ /rate.?limit/i,
73
+ /too.?many.?requests/i,
74
+ /429/,
75
+ /overloaded/i
76
+ ],
77
+ auth_expired: [
78
+ /oauth.*token.*expired/i,
79
+ /authentication.*error/i,
80
+ /invalid.*api.*key/i,
81
+ /unauthorized/i,
82
+ /401/
83
+ ],
84
+ quota_exceeded: [
85
+ /quota.*exceeded/i,
86
+ /usage.*limit/i,
87
+ /credit.*exhausted/i
88
+ ],
89
+ transient: [
90
+ /timeout/i,
91
+ /connection.*reset/i,
92
+ /temporary.*error/i,
93
+ /service.*unavailable/i,
94
+ /503/,
95
+ /502/,
96
+ /504/
97
+ ],
98
+ permanent: [
99
+ /invalid.*model/i,
100
+ /unsupported.*operation/i,
101
+ /not.*found/i,
102
+ /404/,
103
+ /bad.*request/i,
104
+ /400/
105
+ ]
106
+ }
107
+ end
108
+
47
109
  def send_message(prompt:, session: nil)
48
110
  raise "claude CLI not available" unless self.class.available?
49
111
 
@@ -156,6 +218,8 @@ module Aidp
156
218
  TIMEOUT_STATIC_ANALYSIS
157
219
  when /REFACTORING_RECOMMENDATIONS/
158
220
  TIMEOUT_REFACTORING_RECOMMENDATIONS
221
+ when /IMPLEMENTATION/
222
+ TIMEOUT_IMPLEMENTATION
159
223
  else
160
224
  nil # Use default
161
225
  end
@@ -163,16 +227,20 @@ module Aidp
163
227
  end
164
228
 
165
229
  # Check if we should skip permissions based on devcontainer configuration
230
+ # Overrides base class to add logging and Claude-specific config check
166
231
  def should_skip_permissions?
167
- # Check if harness context is available
168
- return false unless @harness_context
232
+ # Use base class devcontainer detection
233
+ if in_devcontainer_or_codespace?
234
+ debug_log("🔓 Detected devcontainer/codespace environment - enabling full permissions", level: :info)
235
+ return true
236
+ end
169
237
 
170
- # Get configuration from harness
171
- config = @harness_context.config
172
- return false unless config
238
+ # Fallback: Check harness context for Claude-specific configuration
239
+ if @harness_context&.config&.respond_to?(:should_use_full_permissions?)
240
+ return @harness_context.config.should_use_full_permissions?("claude")
241
+ end
173
242
 
174
- # Use configuration method to determine if full permissions should be used
175
- config.should_use_full_permissions?("claude")
243
+ false
176
244
  end
177
245
 
178
246
  # Parse stream-json output from Claude CLI
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "tty-prompt"
4
4
  require "tty-spinner"
5
+ require_relative "adapter"
5
6
 
6
7
  module Aidp
7
8
  module Providers
@@ -9,6 +10,7 @@ module Aidp
9
10
 
10
11
  class Base
11
12
  include Aidp::MessageDisplay
13
+ include Aidp::Providers::Adapter
12
14
 
13
15
  # Activity indicator states
14
16
  ACTIVITY_STATES = {
@@ -33,6 +35,7 @@ module Aidp
33
35
  TIMEOUT_DOCUMENTATION_ANALYSIS = 300 # 5 minutes - documentation analysis
34
36
  TIMEOUT_STATIC_ANALYSIS = 450 # 7.5 minutes - static analysis
35
37
  TIMEOUT_REFACTORING_RECOMMENDATIONS = 600 # 10 minutes - refactoring
38
+ TIMEOUT_IMPLEMENTATION = 900 # 15 minutes - implementation (write files, run tests, fix issues)
36
39
 
37
40
  attr_reader :activity_state, :last_activity_time, :start_time, :step_name
38
41
 
@@ -299,7 +302,7 @@ module Aidp
299
302
  error_message = e.message
300
303
 
301
304
  # Check if error is rate limiting
302
- if e.message.match?(/rate.?limit/i) || e.message.match?(/quota/i)
305
+ if e.message.match?(/rate.?limit/i) || e.message.match?(/quota/i) || e.message.match?(/session limit/i)
303
306
  rate_limited = true
304
307
  end
305
308
 
@@ -391,6 +394,31 @@ module Aidp
391
394
  spinner&.stop
392
395
  end
393
396
 
397
+ # Check if we should skip permissions based on devcontainer/codespace environment
398
+ # This enables providers to run with elevated permissions in safe development environments
399
+ # Returns true if running in a devcontainer or GitHub Codespace
400
+ def in_devcontainer_or_codespace?
401
+ ENV["REMOTE_CONTAINERS"] == "true" || ENV["CODESPACES"] == "true"
402
+ end
403
+
404
+ # Check if provider should skip sandbox permissions
405
+ # Providers can override this to add additional logic beyond environment detection
406
+ def should_skip_permissions?
407
+ # First, check for devcontainer/codespace environment (most reliable)
408
+ return true if in_devcontainer_or_codespace?
409
+
410
+ # Fallback: Check if harness context is available and has configuration
411
+ return false unless @harness_context
412
+
413
+ # Get configuration from harness
414
+ config = @harness_context.config
415
+ return false unless config
416
+
417
+ # Use configuration method to determine if full permissions should be used
418
+ # Provider subclasses should pass their provider name
419
+ false # Base implementation returns false, subclasses should override
420
+ end
421
+
394
422
  private
395
423
  end
396
424
  end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Providers
5
+ # CapabilityRegistry maintains a queryable registry of provider capabilities
6
+ # and features. This enables runtime feature detection and provider selection
7
+ # based on required capabilities.
8
+ #
9
+ # @see https://github.com/viamin/aidp/issues/243
10
+ class CapabilityRegistry
11
+ # Standard capability keys
12
+ CAPABILITY_KEYS = [
13
+ :reasoning_tiers, # Array of supported reasoning tiers (mini, standard, thinking, etc.)
14
+ :context_window, # Maximum context window size in tokens
15
+ :supports_json_mode, # Boolean: supports JSON mode output
16
+ :supports_tool_use, # Boolean: supports tool/function calling
17
+ :supports_vision, # Boolean: supports image/vision inputs
18
+ :supports_file_upload, # Boolean: supports file uploads
19
+ :streaming, # Boolean: supports streaming responses
20
+ :supports_mcp, # Boolean: supports Model Context Protocol
21
+ :max_tokens, # Maximum tokens per response
22
+ :supports_dangerous_mode # Boolean: supports elevated permissions mode
23
+ ].freeze
24
+
25
+ def initialize
26
+ @capabilities = {}
27
+ @providers = {}
28
+ end
29
+
30
+ # Register a provider and its capabilities
31
+ # @param provider [Aidp::Providers::Base] provider instance
32
+ # @return [void]
33
+ def register(provider)
34
+ provider_name = provider.name
35
+ @providers[provider_name] = provider
36
+
37
+ # Collect capabilities from provider
38
+ caps = provider.capabilities.dup
39
+ caps[:supports_mcp] = provider.supports_mcp?
40
+ caps[:supports_dangerous_mode] = provider.supports_dangerous_mode?
41
+
42
+ @capabilities[provider_name] = caps
43
+
44
+ Aidp.log_debug("CapabilityRegistry", "registered provider",
45
+ provider: provider_name,
46
+ capabilities: caps.keys)
47
+ end
48
+
49
+ # Unregister a provider
50
+ # @param provider_name [String] provider identifier
51
+ # @return [void]
52
+ def unregister(provider_name)
53
+ @capabilities.delete(provider_name)
54
+ @providers.delete(provider_name)
55
+ end
56
+
57
+ # Get capabilities for a specific provider
58
+ # @param provider_name [String] provider identifier
59
+ # @return [Hash, nil] capabilities hash or nil if not found
60
+ def capabilities_for(provider_name)
61
+ @capabilities[provider_name]
62
+ end
63
+
64
+ # Check if a provider has a specific capability
65
+ # @param provider_name [String] provider identifier
66
+ # @param capability [Symbol] capability key
67
+ # @param value [Object, nil] optional value to match
68
+ # @return [Boolean] true if provider has the capability
69
+ def has_capability?(provider_name, capability, value = nil)
70
+ caps = @capabilities[provider_name]
71
+ return false unless caps
72
+
73
+ if value.nil?
74
+ # Just check if capability exists and is truthy
75
+ caps.key?(capability) && caps[capability]
76
+ else
77
+ # Check if capability matches specific value
78
+ caps[capability] == value
79
+ end
80
+ end
81
+
82
+ # Find providers that match capability requirements
83
+ # @param requirements [Hash] capability requirements
84
+ # @return [Array<String>] array of matching provider names
85
+ # @example
86
+ # registry.find_providers(supports_vision: true, min_context_window: 100_000)
87
+ def find_providers(**requirements)
88
+ matching = []
89
+
90
+ @capabilities.each do |provider_name, caps|
91
+ matches = requirements.all? do |key, required_value|
92
+ case key
93
+ when :min_context_window
94
+ caps[:context_window] && caps[:context_window] >= required_value
95
+ when :max_context_window
96
+ caps[:context_window] && caps[:context_window] <= required_value
97
+ when :reasoning_tier
98
+ caps[:reasoning_tiers]&.include?(required_value)
99
+ else
100
+ # Exact match for boolean and other values
101
+ caps[key] == required_value
102
+ end
103
+ end
104
+
105
+ matching << provider_name if matches
106
+ end
107
+
108
+ matching
109
+ end
110
+
111
+ # Get all registered providers
112
+ # @return [Array<String>] array of provider names
113
+ def registered_providers
114
+ @providers.keys
115
+ end
116
+
117
+ # Get detailed information about all registered providers
118
+ # @return [Hash] provider information indexed by provider name
119
+ def provider_info
120
+ info = {}
121
+
122
+ @providers.each do |provider_name, provider|
123
+ caps = @capabilities[provider_name] || {}
124
+
125
+ info[provider_name] = {
126
+ display_name: provider.display_name,
127
+ available: provider.available?,
128
+ capabilities: caps,
129
+ dangerous_mode_enabled: provider.dangerous_mode_enabled?,
130
+ health_status: provider.health_status
131
+ }
132
+ end
133
+
134
+ info
135
+ end
136
+
137
+ # Check capability compatibility between providers
138
+ # @param provider_name1 [String] first provider
139
+ # @param provider_name2 [String] second provider
140
+ # @return [Hash] compatibility report
141
+ def compatibility_report(provider_name1, provider_name2)
142
+ caps1 = @capabilities[provider_name1]
143
+ caps2 = @capabilities[provider_name2]
144
+
145
+ return {error: "Provider not found"} unless caps1 && caps2
146
+
147
+ common = {}
148
+ differences = {}
149
+
150
+ all_keys = (caps1.keys + caps2.keys).uniq
151
+
152
+ all_keys.each do |key|
153
+ val1 = caps1[key]
154
+ val2 = caps2[key]
155
+
156
+ if val1 == val2
157
+ common[key] = val1
158
+ else
159
+ differences[key] = {provider_name1 => val1, provider_name2 => val2}
160
+ end
161
+ end
162
+
163
+ {
164
+ common_capabilities: common,
165
+ differences: differences,
166
+ compatibility_score: common.size.to_f / all_keys.size
167
+ }
168
+ end
169
+
170
+ # Get capability statistics across all providers
171
+ # @return [Hash] statistics about capability support
172
+ def capability_statistics
173
+ stats = {}
174
+
175
+ CAPABILITY_KEYS.each do |key|
176
+ stats[key] = {
177
+ total_providers: @providers.size,
178
+ supporting_providers: 0,
179
+ providers: []
180
+ }
181
+ end
182
+
183
+ @capabilities.each do |provider_name, caps|
184
+ caps.each do |key, value|
185
+ next unless stats.key?(key)
186
+
187
+ if value.is_a?(TrueClass) || (value.is_a?(Array) && !value.empty?) || (value.is_a?(Integer) && value > 0)
188
+ stats[key][:supporting_providers] += 1
189
+ stats[key][:providers] << provider_name
190
+ end
191
+ end
192
+ end
193
+
194
+ stats
195
+ end
196
+
197
+ # Clear all registered providers
198
+ # @return [void]
199
+ def clear
200
+ @capabilities.clear
201
+ @providers.clear
202
+ end
203
+ end
204
+ end
205
+ end
@@ -79,6 +79,20 @@ module Aidp
79
79
  args += ["--session", session]
80
80
  end
81
81
 
82
+ # In devcontainer, ensure sandbox mode and approval policy are set
83
+ # These are already set via environment variables in devcontainer.json
84
+ # but we verify and log them here for visibility
85
+ if in_devcontainer_or_codespace?
86
+ unless ENV["CODEX_SANDBOX_MODE"] == "danger-full-access"
87
+ ENV["CODEX_SANDBOX_MODE"] = "danger-full-access"
88
+ debug_log("🔓 Set CODEX_SANDBOX_MODE=danger-full-access for devcontainer", level: :info)
89
+ end
90
+ unless ENV["CODEX_APPROVAL_POLICY"] == "never"
91
+ ENV["CODEX_APPROVAL_POLICY"] = "never"
92
+ debug_log("🔓 Set CODEX_APPROVAL_POLICY=never for devcontainer", level: :info)
93
+ end
94
+ end
95
+
82
96
  # Use debug_execute_command for better debugging
83
97
  result = debug_execute_command("codex", args: args, timeout: timeout_seconds, streaming: streaming_enabled)
84
98