aidp 0.23.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aidp/cli.rb +3 -0
  3. data/lib/aidp/execute/work_loop_runner.rb +252 -45
  4. data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
  5. data/lib/aidp/harness/condition_detector.rb +42 -8
  6. data/lib/aidp/harness/config_manager.rb +7 -0
  7. data/lib/aidp/harness/config_schema.rb +25 -0
  8. data/lib/aidp/harness/configuration.rb +69 -6
  9. data/lib/aidp/harness/error_handler.rb +117 -44
  10. data/lib/aidp/harness/provider_manager.rb +64 -0
  11. data/lib/aidp/harness/provider_metrics.rb +138 -0
  12. data/lib/aidp/harness/runner.rb +90 -29
  13. data/lib/aidp/harness/simple_user_interface.rb +4 -0
  14. data/lib/aidp/harness/state/ui_state.rb +0 -10
  15. data/lib/aidp/harness/state_manager.rb +1 -15
  16. data/lib/aidp/harness/test_runner.rb +39 -2
  17. data/lib/aidp/logger.rb +34 -4
  18. data/lib/aidp/providers/adapter.rb +241 -0
  19. data/lib/aidp/providers/anthropic.rb +75 -7
  20. data/lib/aidp/providers/base.rb +29 -1
  21. data/lib/aidp/providers/capability_registry.rb +205 -0
  22. data/lib/aidp/providers/codex.rb +14 -0
  23. data/lib/aidp/providers/error_taxonomy.rb +195 -0
  24. data/lib/aidp/providers/gemini.rb +3 -2
  25. data/lib/aidp/setup/provider_registry.rb +107 -0
  26. data/lib/aidp/setup/wizard.rb +115 -31
  27. data/lib/aidp/version.rb +1 -1
  28. data/lib/aidp/watch/build_processor.rb +263 -23
  29. data/lib/aidp/watch/repository_client.rb +4 -4
  30. data/lib/aidp/watch/runner.rb +37 -5
  31. data/lib/aidp/workflows/guided_agent.rb +53 -0
  32. data/lib/aidp/worktree.rb +67 -10
  33. data/templates/work_loop/decide_whats_next.md +21 -0
  34. data/templates/work_loop/diagnose_failures.md +21 -0
  35. metadata +10 -3
  36. /data/{bin → exe}/aidp +0 -0
@@ -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
 
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Providers
5
+ # ErrorTaxonomy defines the five standardized error categories that all providers
6
+ # use for consistent error handling, retry logic, and escalation.
7
+ #
8
+ # Categories:
9
+ # - rate_limited: Provider is rate-limiting requests (switch provider immediately)
10
+ # - auth_expired: Authentication credentials are invalid or expired (escalate or switch)
11
+ # - quota_exceeded: Usage quota has been exceeded (switch provider)
12
+ # - transient: Temporary error that may resolve on retry (retry with backoff)
13
+ # - permanent: Permanent error that won't resolve with retry (escalate or abort)
14
+ #
15
+ # @see https://github.com/viamin/aidp/issues/243
16
+ module ErrorTaxonomy
17
+ # Error category constants
18
+ RATE_LIMITED = :rate_limited
19
+ AUTH_EXPIRED = :auth_expired
20
+ QUOTA_EXCEEDED = :quota_exceeded
21
+ TRANSIENT = :transient
22
+ PERMANENT = :permanent
23
+
24
+ # All valid error categories
25
+ CATEGORIES = [
26
+ RATE_LIMITED,
27
+ AUTH_EXPIRED,
28
+ QUOTA_EXCEEDED,
29
+ TRANSIENT,
30
+ PERMANENT
31
+ ].freeze
32
+
33
+ # Default error patterns for common error messages
34
+ # Providers can override these with provider-specific patterns
35
+ DEFAULT_PATTERNS = {
36
+ rate_limited: [
37
+ /rate.?limit/i,
38
+ /too.?many.?requests/i,
39
+ /429/,
40
+ /throttl(ed|ing)/i,
41
+ /request.?limit/i,
42
+ /requests.?per.?minute/i,
43
+ /rpm.?exceeded/i
44
+ ],
45
+ auth_expired: [
46
+ /auth(entication|orization).?(fail(ed|ure)|error)/i,
47
+ /invalid.?(api.?key|token|credential)/i,
48
+ /expired.?(api.?key|token|credential)/i,
49
+ /unauthorized/i,
50
+ /401/,
51
+ /403/,
52
+ /permission.?denied/i,
53
+ /access.?denied/i
54
+ ],
55
+ quota_exceeded: [
56
+ /quota.?(exceed(ed)?|limit|exhausted)/i,
57
+ /usage.?limit/i,
58
+ /billing.?limit/i,
59
+ /credit.?limit/i,
60
+ /insufficient.?quota/i,
61
+ /usage.?cap/i
62
+ ],
63
+ transient: [
64
+ /timeout/i,
65
+ /timed?.?out/i,
66
+ /connection.?(reset|refused|lost|closed)/i,
67
+ /temporary.?error/i,
68
+ /try.?again/i,
69
+ /service.?unavailable/i,
70
+ /503/,
71
+ /502/,
72
+ /504/,
73
+ /gateway.?timeout/i,
74
+ /network.?error/i,
75
+ /socket.?error/i,
76
+ /connection.?error/i,
77
+ /broken.?pipe/i,
78
+ /host.?unreachable/i
79
+ ],
80
+ permanent: [
81
+ /invalid.?(model|parameter|request|input)/i,
82
+ /unsupported.?(operation|feature|model)/i,
83
+ /not.?found/i,
84
+ /404/,
85
+ /bad.?request/i,
86
+ /400/,
87
+ /malformed/i,
88
+ /syntax.?error/i,
89
+ /validation.?error/i,
90
+ /model.?not.?available/i,
91
+ /model.?deprecated/i
92
+ ]
93
+ }.freeze
94
+
95
+ # Retry policy for each category
96
+ RETRY_POLICIES = {
97
+ rate_limited: {
98
+ retry: false,
99
+ switch_provider: true,
100
+ escalate: false,
101
+ backoff_strategy: :none
102
+ },
103
+ auth_expired: {
104
+ retry: false,
105
+ switch_provider: true,
106
+ escalate: true,
107
+ backoff_strategy: :none
108
+ },
109
+ quota_exceeded: {
110
+ retry: false,
111
+ switch_provider: true,
112
+ escalate: false,
113
+ backoff_strategy: :none
114
+ },
115
+ transient: {
116
+ retry: true,
117
+ switch_provider: false,
118
+ escalate: false,
119
+ backoff_strategy: :exponential
120
+ },
121
+ permanent: {
122
+ retry: false,
123
+ switch_provider: false,
124
+ escalate: true,
125
+ backoff_strategy: :none
126
+ }
127
+ }.freeze
128
+
129
+ # Check if a category is valid
130
+ # @param category [Symbol] category to check
131
+ # @return [Boolean] true if valid
132
+ def self.valid_category?(category)
133
+ CATEGORIES.include?(category)
134
+ end
135
+
136
+ # Get retry policy for a category
137
+ # @param category [Symbol] error category
138
+ # @return [Hash] retry policy configuration
139
+ def self.retry_policy(category)
140
+ RETRY_POLICIES[category] || RETRY_POLICIES[:transient]
141
+ end
142
+
143
+ # Classify an error message using default patterns
144
+ # @param message [String] error message
145
+ # @return [Symbol] error category
146
+ def self.classify_message(message)
147
+ return :transient if message.nil? || message.empty?
148
+
149
+ message_lower = message.downcase
150
+
151
+ # Check each category's patterns
152
+ DEFAULT_PATTERNS.each do |category, patterns|
153
+ patterns.each do |pattern|
154
+ return category if message_lower.match?(pattern)
155
+ end
156
+ end
157
+
158
+ # Default to transient for unknown errors
159
+ :transient
160
+ end
161
+
162
+ # Check if an error category is retryable
163
+ # @param category [Symbol] error category
164
+ # @return [Boolean] true if should retry
165
+ def self.retryable?(category)
166
+ policy = retry_policy(category)
167
+ policy[:retry] == true
168
+ end
169
+
170
+ # Check if an error category should trigger provider switch
171
+ # @param category [Symbol] error category
172
+ # @return [Boolean] true if should switch provider
173
+ def self.should_switch_provider?(category)
174
+ policy = retry_policy(category)
175
+ policy[:switch_provider] == true
176
+ end
177
+
178
+ # Check if an error category should be escalated
179
+ # @param category [Symbol] error category
180
+ # @return [Boolean] true if should escalate
181
+ def self.should_escalate?(category)
182
+ policy = retry_policy(category)
183
+ policy[:escalate] == true
184
+ end
185
+
186
+ # Get backoff strategy for a category
187
+ # @param category [Symbol] error category
188
+ # @return [Symbol] backoff strategy (:none, :linear, :exponential)
189
+ def self.backoff_strategy(category)
190
+ policy = retry_policy(category)
191
+ policy[:backoff_strategy] || :none
192
+ end
193
+ end
194
+ end
195
+ end
@@ -36,11 +36,12 @@ module Aidp
36
36
  end
37
37
 
38
38
  begin
39
+ command_args = ["--prompt", prompt]
39
40
  # Use debug_execute_command with streaming support
40
- result = debug_execute_command("gemini", args: ["--print"], input: prompt, timeout: timeout_seconds, streaming: streaming_enabled)
41
+ result = debug_execute_command("gemini", args: command_args, timeout: timeout_seconds, streaming: streaming_enabled)
41
42
 
42
43
  # Log the results
43
- debug_command("gemini", args: ["--print"], input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
44
+ debug_command("gemini", args: command_args, input: nil, output: result.out, error: result.err, exit_code: result.exit_status)
44
45
 
45
46
  if result.exit_status == 0
46
47
  result.out