aidp 0.23.0 → 0.25.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -1
  3. data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
  4. data/lib/aidp/auto_update/checkpoint.rb +178 -0
  5. data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
  6. data/lib/aidp/auto_update/coordinator.rb +204 -0
  7. data/lib/aidp/auto_update/errors.rb +17 -0
  8. data/lib/aidp/auto_update/failure_tracker.rb +162 -0
  9. data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
  10. data/lib/aidp/auto_update/update_check.rb +106 -0
  11. data/lib/aidp/auto_update/update_logger.rb +143 -0
  12. data/lib/aidp/auto_update/update_policy.rb +109 -0
  13. data/lib/aidp/auto_update/version_detector.rb +144 -0
  14. data/lib/aidp/auto_update.rb +52 -0
  15. data/lib/aidp/cli.rb +168 -1
  16. data/lib/aidp/execute/work_loop_runner.rb +252 -45
  17. data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
  18. data/lib/aidp/harness/condition_detector.rb +42 -8
  19. data/lib/aidp/harness/config_manager.rb +7 -0
  20. data/lib/aidp/harness/config_schema.rb +75 -0
  21. data/lib/aidp/harness/configuration.rb +69 -6
  22. data/lib/aidp/harness/error_handler.rb +117 -44
  23. data/lib/aidp/harness/provider_factory.rb +2 -0
  24. data/lib/aidp/harness/provider_manager.rb +64 -0
  25. data/lib/aidp/harness/provider_metrics.rb +138 -0
  26. data/lib/aidp/harness/runner.rb +90 -29
  27. data/lib/aidp/harness/simple_user_interface.rb +4 -0
  28. data/lib/aidp/harness/state/ui_state.rb +0 -10
  29. data/lib/aidp/harness/state_manager.rb +1 -15
  30. data/lib/aidp/harness/test_runner.rb +39 -2
  31. data/lib/aidp/logger.rb +34 -4
  32. data/lib/aidp/message_display.rb +10 -2
  33. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
  34. data/lib/aidp/provider_manager.rb +2 -0
  35. data/lib/aidp/providers/adapter.rb +241 -0
  36. data/lib/aidp/providers/anthropic.rb +75 -7
  37. data/lib/aidp/providers/base.rb +29 -1
  38. data/lib/aidp/providers/capability_registry.rb +205 -0
  39. data/lib/aidp/providers/codex.rb +14 -0
  40. data/lib/aidp/providers/error_taxonomy.rb +195 -0
  41. data/lib/aidp/providers/gemini.rb +3 -2
  42. data/lib/aidp/providers/kilocode.rb +202 -0
  43. data/lib/aidp/setup/provider_registry.rb +122 -0
  44. data/lib/aidp/setup/wizard.rb +125 -33
  45. data/lib/aidp/skills/composer.rb +4 -0
  46. data/lib/aidp/skills/loader.rb +3 -1
  47. data/lib/aidp/version.rb +1 -1
  48. data/lib/aidp/watch/build_processor.rb +323 -33
  49. data/lib/aidp/watch/ci_fix_processor.rb +448 -0
  50. data/lib/aidp/watch/plan_processor.rb +12 -2
  51. data/lib/aidp/watch/repository_client.rb +384 -4
  52. data/lib/aidp/watch/review_processor.rb +266 -0
  53. data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
  54. data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
  55. data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
  56. data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
  57. data/lib/aidp/watch/runner.rb +222 -5
  58. data/lib/aidp/watch/state_store.rb +53 -0
  59. data/lib/aidp/workflows/guided_agent.rb +53 -0
  60. data/lib/aidp/worktree.rb +67 -10
  61. data/lib/aidp.rb +1 -0
  62. data/templates/work_loop/decide_whats_next.md +21 -0
  63. data/templates/work_loop/diagnose_failures.md +21 -0
  64. metadata +29 -3
  65. /data/{bin → exe}/aidp +0 -0
@@ -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
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+ require_relative "base"
5
+ require_relative "../util"
6
+ require_relative "../debug_mixin"
7
+
8
+ module Aidp
9
+ module Providers
10
+ class Kilocode < Base
11
+ include Aidp::DebugMixin
12
+
13
+ def self.available?
14
+ !!Aidp::Util.which("kilocode")
15
+ end
16
+
17
+ def name
18
+ "kilocode"
19
+ end
20
+
21
+ def display_name
22
+ "Kilocode"
23
+ end
24
+
25
+ def send_message(prompt:, session: nil)
26
+ raise "kilocode not available" unless self.class.available?
27
+
28
+ # Smart timeout calculation
29
+ timeout_seconds = calculate_timeout
30
+
31
+ debug_provider("kilocode", "Starting execution", {timeout: timeout_seconds})
32
+ debug_log("📝 Sending prompt to kilocode (length: #{prompt.length})", level: :info)
33
+
34
+ # Check if streaming mode is enabled
35
+ streaming_enabled = ENV["AIDP_STREAMING"] == "1" || ENV["DEBUG"] == "1"
36
+ if streaming_enabled
37
+ display_message("📺 Display streaming enabled - output buffering reduced", type: :info)
38
+ end
39
+
40
+ # Check if prompt is too large and warn
41
+ if prompt.length > 3000
42
+ debug_log("⚠️ Large prompt detected (#{prompt.length} chars) - this may cause rate limiting", level: :warn)
43
+ end
44
+
45
+ # Set up activity monitoring
46
+ setup_activity_monitoring("kilocode", method(:activity_callback))
47
+ record_activity("Starting kilocode execution")
48
+
49
+ # Create a spinner for activity display
50
+ spinner = TTY::Spinner.new("[:spinner] :title", format: :dots, hide_cursor: true)
51
+ spinner.auto_spin
52
+
53
+ activity_display_thread = Thread.new do
54
+ start_time = Time.now
55
+ loop do
56
+ sleep 0.5 # Update every 500ms to reduce spam
57
+ elapsed = Time.now - start_time
58
+
59
+ # Break if we've been running too long or state changed
60
+ break if elapsed > timeout_seconds || @activity_state == :completed || @activity_state == :failed
61
+
62
+ update_spinner_status(spinner, elapsed, "🔄 kilocode")
63
+ end
64
+ end
65
+
66
+ begin
67
+ # Build kilocode command arguments
68
+ args = ["--auto"]
69
+
70
+ # Add model if specified
71
+ model = ENV["KILOCODE_MODEL"]
72
+ if model
73
+ args.concat(["-m", model])
74
+ end
75
+
76
+ # Add workspace detection if needed
77
+ if Dir.exist?(".git") && ENV["KILOCODE_WORKSPACE"]
78
+ args.concat(["--workspace", ENV["KILOCODE_WORKSPACE"]])
79
+ end
80
+
81
+ # Set authentication via environment variable
82
+ env_vars = {}
83
+ if ENV["KILOCODE_TOKEN"]
84
+ env_vars["KILOCODE_TOKEN"] = ENV["KILOCODE_TOKEN"]
85
+ end
86
+
87
+ # Use debug_execute_command for better debugging
88
+ result = debug_execute_command("kilocode", args: args, input: prompt, timeout: timeout_seconds, streaming: streaming_enabled, env: env_vars)
89
+
90
+ # Log the results
91
+ debug_command("kilocode", args: args, input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
92
+
93
+ if result.exit_status == 0
94
+ spinner.success("✓")
95
+ mark_completed
96
+ result.out
97
+ else
98
+ spinner.error("✗")
99
+ mark_failed("kilocode failed with exit code #{result.exit_status}")
100
+ debug_error(StandardError.new("kilocode failed"), {exit_code: result.exit_status, stderr: result.err})
101
+ raise "kilocode failed with exit code #{result.exit_status}: #{result.err}"
102
+ end
103
+ rescue => e
104
+ spinner&.error("✗")
105
+ mark_failed("kilocode execution failed: #{e.message}")
106
+ debug_error(e, {provider: "kilocode", prompt_length: prompt.length})
107
+ raise
108
+ ensure
109
+ cleanup_activity_display(activity_display_thread, spinner)
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def calculate_timeout
116
+ # Priority order for timeout calculation:
117
+ # 1. Quick mode (for testing)
118
+ # 2. Environment variable override
119
+ # 3. Adaptive timeout based on step type
120
+ # 4. Default timeout
121
+
122
+ if ENV["AIDP_QUICK_MODE"]
123
+ display_message("⚡ Quick mode enabled - #{TIMEOUT_QUICK_MODE / 60} minute timeout", type: :highlight)
124
+ return TIMEOUT_QUICK_MODE
125
+ end
126
+
127
+ if ENV["AIDP_KILOCODE_TIMEOUT"]
128
+ return ENV["AIDP_KILOCODE_TIMEOUT"].to_i
129
+ end
130
+
131
+ if adaptive_timeout
132
+ display_message("🧠 Using adaptive timeout: #{adaptive_timeout} seconds", type: :info)
133
+ return adaptive_timeout
134
+ end
135
+
136
+ # Default timeout
137
+ display_message("📋 Using default timeout: #{TIMEOUT_DEFAULT / 60} minutes", type: :info)
138
+ TIMEOUT_DEFAULT
139
+ end
140
+
141
+ def adaptive_timeout
142
+ @adaptive_timeout ||= begin
143
+ # Timeout recommendations based on step type patterns
144
+ step_name = ENV["AIDP_CURRENT_STEP"] || ""
145
+
146
+ case step_name
147
+ when /REPOSITORY_ANALYSIS/
148
+ TIMEOUT_REPOSITORY_ANALYSIS
149
+ when /ARCHITECTURE_ANALYSIS/
150
+ TIMEOUT_ARCHITECTURE_ANALYSIS
151
+ when /TEST_ANALYSIS/
152
+ TIMEOUT_TEST_ANALYSIS
153
+ when /FUNCTIONALITY_ANALYSIS/
154
+ TIMEOUT_FUNCTIONALITY_ANALYSIS
155
+ when /DOCUMENTATION_ANALYSIS/
156
+ TIMEOUT_DOCUMENTATION_ANALYSIS
157
+ when /STATIC_ANALYSIS/
158
+ TIMEOUT_STATIC_ANALYSIS
159
+ when /REFACTORING_RECOMMENDATIONS/
160
+ TIMEOUT_REFACTORING_RECOMMENDATIONS
161
+ else
162
+ nil # Use default
163
+ end
164
+ end
165
+ end
166
+
167
+ def activity_callback(state, message, provider)
168
+ # This is now handled by the animated display thread
169
+ # Only print static messages for state changes
170
+ case state
171
+ when :starting
172
+ display_message("🚀 Starting kilocode execution...", type: :info)
173
+ when :completed
174
+ display_message("✅ kilocode execution completed", type: :success)
175
+ when :failed
176
+ display_message("❌ kilocode execution failed: #{message}", type: :error)
177
+ end
178
+ end
179
+
180
+ def setup_activity_monitoring(provider_name, callback)
181
+ @activity_callback = callback
182
+ @activity_state = :starting
183
+ @activity_start_time = Time.now
184
+ end
185
+
186
+ def record_activity(message)
187
+ @activity_state = :running
188
+ @activity_callback&.call(:running, message, "kilocode")
189
+ end
190
+
191
+ def mark_completed
192
+ @activity_state = :completed
193
+ @activity_callback&.call(:completed, "Execution completed", "kilocode")
194
+ end
195
+
196
+ def mark_failed(reason)
197
+ @activity_state = :failed
198
+ @activity_callback&.call(:failed, reason, "kilocode")
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Setup
5
+ # Centralized registry for provider metadata including billing types and model families.
6
+ # This module provides a single source of truth for provider configuration options.
7
+ module ProviderRegistry
8
+ # Billing type options for providers
9
+ BILLING_TYPES = [
10
+ {
11
+ label: "Subscription / flat-rate",
12
+ value: "subscription",
13
+ description: "Monthly or annual subscription with unlimited usage"
14
+ },
15
+ {
16
+ label: "Usage-based / metered (API)",
17
+ value: "usage_based",
18
+ description: "Pay per API call or token usage"
19
+ },
20
+ {
21
+ label: "Passthrough / local (no billing)",
22
+ value: "passthrough",
23
+ description: "Local execution or proxy without direct billing"
24
+ }
25
+ ].freeze
26
+
27
+ # Model family options for providers
28
+ MODEL_FAMILIES = [
29
+ {
30
+ label: "Auto (let provider decide)",
31
+ value: "auto",
32
+ description: "Use provider's default model selection"
33
+ },
34
+ {
35
+ label: "OpenAI o-series (reasoning models)",
36
+ value: "openai_o",
37
+ description: "Advanced reasoning capabilities, slower but more thorough"
38
+ },
39
+ {
40
+ label: "Anthropic Claude (balanced)",
41
+ value: "claude",
42
+ description: "Balanced performance for general-purpose tasks"
43
+ },
44
+ {
45
+ label: "Google Gemini (multimodal)",
46
+ value: "gemini",
47
+ description: "Google's multimodal AI with strong reasoning and vision capabilities"
48
+ },
49
+ {
50
+ label: "Meta Llama (open-source)",
51
+ value: "llama",
52
+ description: "Meta's open-source model family, suitable for self-hosting"
53
+ },
54
+ {
55
+ label: "DeepSeek (efficient reasoning)",
56
+ value: "deepseek",
57
+ description: "Cost-efficient reasoning models with strong performance"
58
+ },
59
+ {
60
+ label: "Mistral (European/open)",
61
+ value: "mistral",
62
+ description: "European provider with open-source focus"
63
+ },
64
+ {
65
+ label: "Local LLM (self-hosted)",
66
+ value: "local",
67
+ description: "Self-hosted or local model execution"
68
+ }
69
+ ].freeze
70
+
71
+ # Returns array of [label, value] pairs for billing types
72
+ def self.billing_type_choices
73
+ BILLING_TYPES.map { |bt| [bt[:label], bt[:value]] }
74
+ end
75
+
76
+ # Returns array of [label, value] pairs for model families
77
+ def self.model_family_choices
78
+ MODEL_FAMILIES.map { |mf| [mf[:label], mf[:value]] }
79
+ end
80
+
81
+ # Finds label for a given billing type value
82
+ def self.billing_type_label(value)
83
+ BILLING_TYPES.find { |bt| bt[:value] == value }&.dig(:label) || value
84
+ end
85
+
86
+ # Finds label for a given model family value
87
+ def self.model_family_label(value)
88
+ MODEL_FAMILIES.find { |mf| mf[:value] == value }&.dig(:label) || value
89
+ end
90
+
91
+ # Finds description for a given billing type value
92
+ def self.billing_type_description(value)
93
+ BILLING_TYPES.find { |bt| bt[:value] == value }&.dig(:description)
94
+ end
95
+
96
+ # Finds description for a given model family value
97
+ def self.model_family_description(value)
98
+ MODEL_FAMILIES.find { |mf| mf[:value] == value }&.dig(:description)
99
+ end
100
+
101
+ # Validates if a billing type value is valid
102
+ def self.valid_billing_type?(value)
103
+ BILLING_TYPES.any? { |bt| bt[:value] == value }
104
+ end
105
+
106
+ # Validates if a model family value is valid
107
+ def self.valid_model_family?(value)
108
+ MODEL_FAMILIES.any? { |mf| mf[:value] == value }
109
+ end
110
+
111
+ # Returns all valid billing type values
112
+ def self.billing_type_values
113
+ BILLING_TYPES.map { |bt| bt[:value] }
114
+ end
115
+
116
+ # Returns all valid model family values
117
+ def self.model_family_values
118
+ MODEL_FAMILIES.map { |mf| mf[:value] }
119
+ end
120
+ end
121
+ end
122
+ end