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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
3
4
  require_relative "deterministic_unit"
4
5
  require_relative "../logger"
5
6
 
@@ -18,18 +19,21 @@ module Aidp
18
19
 
19
20
  attr_reader :last_agentic_summary
20
21
 
21
- def initialize(units_config, clock: Time)
22
+ def initialize(units_config, project_dir:, clock: Time)
22
23
  @clock = clock
24
+ @project_dir = project_dir
23
25
  @deterministic_definitions = build_deterministic_definitions(units_config[:deterministic])
24
26
  @defaults = default_options.merge(units_config[:defaults] || {})
25
27
  @pending_units = []
28
+ @initial_unit_requests = read_initial_unit_requests
26
29
  @deterministic_history = []
27
30
  @deterministic_state = Hash.new { |h, key| h[key] = default_deterministic_state }
28
31
  @agentic_runs = []
29
32
  @last_agentic_summary = nil
30
33
  @consecutive_deciders = 0
31
34
  @completed = false
32
- @started = false
35
+ apply_initial_requests
36
+ @started = @pending_units.any?
33
37
  end
34
38
 
35
39
  def next_unit
@@ -173,6 +177,27 @@ module Aidp
173
177
  end
174
178
  end
175
179
 
180
+ def read_initial_unit_requests
181
+ return [] unless @project_dir
182
+
183
+ path = File.join(@project_dir, ".aidp", "work_loop", "initial_units.txt")
184
+ return [] unless File.exist?(path)
185
+
186
+ requests = File.readlines(path, chomp: true).map(&:strip).reject(&:empty?)
187
+ File.delete(path)
188
+ requests
189
+ rescue => e
190
+ Aidp.logger.warn("work_loop", "Failed to read initial work loop requests", error: e.message)
191
+ []
192
+ end
193
+
194
+ def apply_initial_requests
195
+ Array(@initial_unit_requests).each do |request|
196
+ queue_requested_unit(request.to_sym)
197
+ end
198
+ @initial_unit_requests = []
199
+ end
200
+
176
201
  def default_deterministic_state
177
202
  {last_run_at: nil, current_backoff: nil}
178
203
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "time"
4
+
3
5
  module Aidp
4
6
  module Harness
5
7
  # Detects run conditions (rate limits, user feedback, completion, errors)
@@ -15,14 +17,16 @@ module Aidp
15
17
  /429/i,
16
18
  /rate.{0,20}exceeded/i,
17
19
  /throttled/i,
18
- /limit.{0,20}exceeded/i
20
+ /limit.{0,20}exceeded/i,
21
+ /session limit/i
19
22
  ],
20
23
  # Anthropic/Claude specific
21
24
  anthropic: [
22
25
  /rate limit exceeded/i,
23
26
  /too many requests/i,
24
27
  /quota.{0,20}exceeded/i,
25
- /anthropic.{0,20}rate.{0,20}limit/i
28
+ /anthropic.{0,20}rate.{0,20}limit/i,
29
+ /session limit reached/i
26
30
  ],
27
31
  # OpenAI specific
28
32
  openai: [
@@ -143,10 +147,10 @@ module Aidp
143
147
 
144
148
  # Rate limit reset time patterns
145
149
  @reset_time_patterns = [
146
- /reset.{0,20}in.{0,20}(\d+).{0,20}seconds/i,
147
- /retry.{0,20}after.{0,20}(\d+).{0,20}seconds/i,
150
+ /reset(?:s)?\s+in\s+(\d+)\s+seconds/i,
151
+ /retry\s+after\s+(\d+)\s+seconds/i,
148
152
  /wait[^\d]*(\d+)[^\d]*seconds/i,
149
- /(\d+).{0,20}seconds.{0,20}until.{0,20}reset/i,
153
+ /(\d+)\s+seconds\s+until\s+reset/i,
150
154
  /reset.{0,20}at.{0,20}(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/i,
151
155
  /retry.{0,20}after.{0,20}(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/i
152
156
  ]
@@ -168,7 +172,18 @@ module Aidp
168
172
  result[:output],
169
173
  result[:response],
170
174
  result[:body]
171
- ].compact.join(" ")
175
+ ].compact.join(" ").strip
176
+
177
+ return nil if text_content.empty?
178
+
179
+ {
180
+ provider: provider,
181
+ detected_at: Time.now,
182
+ reset_time: extract_reset_time(text_content),
183
+ retry_after: extract_retry_after(text_content),
184
+ limit_type: detect_limit_type(text_content, provider),
185
+ message: text_content
186
+ }
172
187
 
173
188
  return false if text_content.empty?
174
189
 
@@ -205,17 +220,34 @@ module Aidp
205
220
 
206
221
  # Extract reset time from rate limit message
207
222
  def extract_reset_time(text_content)
223
+ # Handle expressions like "resets 4am" or "reset at 4:30pm"
224
+ time_of_day_match = text_content.match(/reset(?:s)?(?:\s+at)?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i)
225
+ if time_of_day_match
226
+ hour = time_of_day_match[1].to_i
227
+ minute = time_of_day_match[2] ? time_of_day_match[2].to_i : 0
228
+ meridiem = time_of_day_match[3].downcase
229
+
230
+ hour %= 12
231
+ hour += 12 if meridiem == "pm"
232
+
233
+ now = Time.now
234
+ reset_time = Time.new(now.year, now.month, now.day, hour, minute, 0, now.utc_offset)
235
+ reset_time += 86_400 if reset_time <= now
236
+ return reset_time
237
+ end
238
+
208
239
  @reset_time_patterns.each do |pattern|
209
240
  match = text_content.match(pattern)
210
241
  next unless match
211
242
 
212
243
  if match[1].match?(/^\d+$/)
213
244
  # Seconds from now
214
- Time.now + match[1].to_i
245
+ return Time.now + match[1].to_i
215
246
  else
216
247
  # Specific timestamp
217
248
  begin
218
- Time.parse(match[1])
249
+ parsed_time = Time.parse(match[1])
250
+ return parsed_time if parsed_time
219
251
  rescue ArgumentError
220
252
  nil
221
253
  end
@@ -246,6 +278,8 @@ module Aidp
246
278
 
247
279
  # Detect the type of rate limit
248
280
  def detect_limit_type(text_content, provider)
281
+ return "session_limit" if text_content.match?(/session limit/i)
282
+
249
283
  case provider&.to_s&.downcase
250
284
  when "anthropic", "claude"
251
285
  if text_content.match?(/requests per minute/i)
@@ -10,6 +10,8 @@ module Aidp
10
10
  class ConfigManager
11
11
  include ProviderTypeChecker
12
12
 
13
+ attr_reader :project_dir
14
+
13
15
  def initialize(project_dir = Dir.pwd)
14
16
  @project_dir = project_dir
15
17
  @loader = ConfigLoader.new(project_dir)
@@ -101,6 +103,11 @@ module Aidp
101
103
  }
102
104
  end
103
105
 
106
+ # Get max retries (alias for backward compatibility with ErrorHandler)
107
+ def max_retries(options = {})
108
+ retry_config(options)[:max_attempts]
109
+ end
110
+
104
111
  # Get circuit breaker configuration
105
112
  def circuit_breaker_config(options = {})
106
113
  harness_config = harness_config(options)
@@ -886,6 +886,31 @@ module Aidp
886
886
  type: :string
887
887
  }
888
888
  },
889
+ dangerous_mode: {
890
+ type: :hash,
891
+ required: false,
892
+ default: {},
893
+ properties: {
894
+ enabled: {
895
+ type: :boolean,
896
+ required: false,
897
+ default: false
898
+ },
899
+ flags: {
900
+ type: :array,
901
+ required: false,
902
+ default: [],
903
+ items: {
904
+ type: :string
905
+ }
906
+ },
907
+ auto_enable_in_devcontainer: {
908
+ type: :boolean,
909
+ required: false,
910
+ default: true
911
+ }
912
+ }
913
+ },
889
914
  models: {
890
915
  type: :array,
891
916
  required: false,
@@ -7,6 +7,8 @@ module Aidp
7
7
  module Harness
8
8
  # Handles loading and validation of harness configuration from aidp.yml
9
9
  class Configuration
10
+ attr_reader :project_dir
11
+
10
12
  def initialize(project_dir)
11
13
  @project_dir = project_dir
12
14
  @config = Aidp::Config.load_harness_config(project_dir)
@@ -600,7 +602,16 @@ module Aidp
600
602
 
601
603
  # Get devcontainer configuration
602
604
  def devcontainer_config
603
- @config[:devcontainer] || default_devcontainer_config
605
+ return @devcontainer_config if defined?(@devcontainer_config)
606
+
607
+ raw_config = @config[:devcontainer] || @config["devcontainer"]
608
+ base = deep_dup(default_devcontainer_config)
609
+
610
+ @devcontainer_config = if raw_config.is_a?(Hash)
611
+ deep_merge_hashes(base, deep_symbolize_keys(raw_config))
612
+ else
613
+ base
614
+ end
604
615
  end
605
616
 
606
617
  # Check if devcontainer features are enabled
@@ -629,7 +640,10 @@ module Aidp
629
640
 
630
641
  # Get devcontainer permissions config
631
642
  def devcontainer_permissions
632
- devcontainer_config[:permissions] || {}
643
+ permissions = devcontainer_config[:permissions]
644
+ return {} unless permissions.is_a?(Hash)
645
+
646
+ permissions.transform_keys { |key| key.to_sym }
633
647
  end
634
648
 
635
649
  # Check if dangerous filesystem operations are allowed in devcontainer
@@ -639,7 +653,15 @@ module Aidp
639
653
 
640
654
  # Get list of providers that should skip permission checks in devcontainer
641
655
  def devcontainer_skip_permission_checks
642
- devcontainer_permissions[:skip_permission_checks] || []
656
+ permissions = devcontainer_config[:permissions]
657
+ list = nil
658
+
659
+ if permissions.is_a?(Hash)
660
+ list = permissions[:skip_permission_checks] || permissions["skip_permission_checks"]
661
+ end
662
+
663
+ list = default_skip_permission_checks if list.nil?
664
+ Array(list).map(&:to_s)
643
665
  end
644
666
 
645
667
  # Check if a specific provider should skip permission checks in devcontainer
@@ -843,7 +865,7 @@ module Aidp
843
865
  max_backoff_seconds: 1800,
844
866
  next: {
845
867
  success: :agentic,
846
- failure: :decide_whats_next,
868
+ failure: :diagnose_failures,
847
869
  else: :decide_whats_next
848
870
  }
849
871
  },
@@ -856,7 +878,7 @@ module Aidp
856
878
  max_backoff_seconds: 1800,
857
879
  next: {
858
880
  success: :agentic,
859
- failure: :decide_whats_next,
881
+ failure: :diagnose_failures,
860
882
  else: :decide_whats_next
861
883
  }
862
884
  },
@@ -1055,7 +1077,7 @@ module Aidp
1055
1077
  force_detection: nil,
1056
1078
  permissions: {
1057
1079
  dangerous_filesystem_ops: false,
1058
- skip_permission_checks: []
1080
+ skip_permission_checks: ["claude"]
1059
1081
  },
1060
1082
  settings: {
1061
1083
  timeout_multiplier: 1.0,
@@ -1065,6 +1087,47 @@ module Aidp
1065
1087
  }
1066
1088
  end
1067
1089
 
1090
+ def default_skip_permission_checks
1091
+ Array(default_devcontainer_config.dig(:permissions, :skip_permission_checks)).map(&:to_s)
1092
+ end
1093
+
1094
+ def deep_symbolize_keys(value)
1095
+ case value
1096
+ when Hash
1097
+ value.each_with_object({}) do |(key, val), memo|
1098
+ memo[key.to_sym] = deep_symbolize_keys(val)
1099
+ end
1100
+ when Array
1101
+ value.map { |item| deep_symbolize_keys(item) }
1102
+ else
1103
+ value
1104
+ end
1105
+ end
1106
+
1107
+ def deep_merge_hashes(base, overrides)
1108
+ overrides.each do |key, value|
1109
+ base[key] = if base[key].is_a?(Hash) && value.is_a?(Hash)
1110
+ deep_merge_hashes(base[key], value)
1111
+ else
1112
+ value
1113
+ end
1114
+ end
1115
+ base
1116
+ end
1117
+
1118
+ def deep_dup(value)
1119
+ case value
1120
+ when Hash
1121
+ value.each_with_object({}) do |(key, val), memo|
1122
+ memo[key] = deep_dup(val)
1123
+ end
1124
+ when Array
1125
+ value.map { |item| deep_dup(item) }
1126
+ else
1127
+ value
1128
+ end
1129
+ end
1130
+
1068
1131
  # Custom error class for configuration issues
1069
1132
  class ConfigurationError < StandardError; end
1070
1133
  end
@@ -3,6 +3,7 @@
3
3
  require "net/http"
4
4
  require_relative "../debug_mixin"
5
5
  require_relative "../concurrency"
6
+ require_relative "../providers/error_taxonomy"
6
7
 
7
8
  module Aidp
8
9
  module Harness
@@ -252,9 +253,10 @@ module Aidp
252
253
  # Check if we should retry based on error type and strategy
253
254
  def should_retry?(error_info, strategy)
254
255
  return false unless strategy[:enabled]
255
- return false if error_info[:error_type] == :rate_limit
256
- return false if error_info[:error_type] == :authentication
257
- return false if error_info[:error_type] == :permission_denied
256
+
257
+ # Use ErrorTaxonomy to determine if error is retryable
258
+ error_type = error_info[:error_type]
259
+ return false unless Aidp::Providers::ErrorTaxonomy.retryable?(error_type)
258
260
 
259
261
  # Check circuit breaker
260
262
  circuit_breaker_key = "#{error_info[:provider]}:#{error_info[:model]}"
@@ -337,7 +339,62 @@ module Aidp
337
339
 
338
340
  def initialize_retry_strategies
339
341
  @retry_strategies = {
340
- # Network errors - retry with exponential backoff
342
+ # Transient errors - retry with exponential backoff
343
+ transient: {
344
+ name: "transient",
345
+ enabled: true,
346
+ max_retries: 3,
347
+ backoff_strategy: :exponential,
348
+ base_delay: 1.0,
349
+ max_delay: 30.0,
350
+ jitter: true
351
+ },
352
+
353
+ # Rate limited errors - no retry, immediate switch
354
+ rate_limited: {
355
+ name: "rate_limited",
356
+ enabled: false,
357
+ max_retries: 0,
358
+ backoff_strategy: :none,
359
+ base_delay: 0.0,
360
+ max_delay: 0.0,
361
+ jitter: false
362
+ },
363
+
364
+ # Authentication expired - no retry, switch provider
365
+ auth_expired: {
366
+ name: "auth_expired",
367
+ enabled: false,
368
+ max_retries: 0,
369
+ backoff_strategy: :none,
370
+ base_delay: 0.0,
371
+ max_delay: 0.0,
372
+ jitter: false
373
+ },
374
+
375
+ # Quota exceeded - no retry, switch provider
376
+ quota_exceeded: {
377
+ name: "quota_exceeded",
378
+ enabled: false,
379
+ max_retries: 0,
380
+ backoff_strategy: :none,
381
+ base_delay: 0.0,
382
+ max_delay: 0.0,
383
+ jitter: false
384
+ },
385
+
386
+ # Permanent errors - no retry, escalate
387
+ permanent: {
388
+ name: "permanent",
389
+ enabled: false,
390
+ max_retries: 0,
391
+ backoff_strategy: :none,
392
+ base_delay: 0.0,
393
+ max_delay: 0.0,
394
+ jitter: false
395
+ },
396
+
397
+ # Legacy aliases for backward compatibility
341
398
  network_error: {
342
399
  name: "network_error",
343
400
  enabled: true,
@@ -347,8 +404,6 @@ module Aidp
347
404
  max_delay: 30.0,
348
405
  jitter: true
349
406
  },
350
-
351
- # Server errors - retry with linear backoff
352
407
  server_error: {
353
408
  name: "server_error",
354
409
  enabled: true,
@@ -358,8 +413,6 @@ module Aidp
358
413
  max_delay: 10.0,
359
414
  jitter: true
360
415
  },
361
-
362
- # Timeout errors - retry with exponential backoff
363
416
  timeout: {
364
417
  name: "timeout",
365
418
  enabled: true,
@@ -369,8 +422,6 @@ module Aidp
369
422
  max_delay: 15.0,
370
423
  jitter: true
371
424
  },
372
-
373
- # Rate limit errors - no retry, immediate switch
374
425
  rate_limit: {
375
426
  name: "rate_limit",
376
427
  enabled: false,
@@ -380,8 +431,6 @@ module Aidp
380
431
  max_delay: 0.0,
381
432
  jitter: false
382
433
  },
383
-
384
- # Authentication errors - no retry, escalate
385
434
  authentication: {
386
435
  name: "authentication",
387
436
  enabled: false,
@@ -391,8 +440,6 @@ module Aidp
391
440
  max_delay: 0.0,
392
441
  jitter: false
393
442
  },
394
-
395
- # Permission denied - no retry, escalate
396
443
  permission_denied: {
397
444
  name: "permission_denied",
398
445
  enabled: false,
@@ -586,41 +633,36 @@ module Aidp
586
633
  private
587
634
 
588
635
  def classify_error_type(error)
589
- return :unknown if error.nil?
636
+ return :transient if error.nil?
637
+
638
+ # Use standardized error taxonomy for classification
639
+ message = error.message.to_s
590
640
 
641
+ # First, use ErrorTaxonomy to classify by message
642
+ category = Aidp::Providers::ErrorTaxonomy.classify_message(message)
643
+
644
+ # Override with more specific classification based on error type
591
645
  case error
592
646
  when Timeout::Error
593
- :timeout
647
+ :transient
594
648
  when Net::HTTPError
595
649
  case error.response.code.to_i
596
650
  when 429
597
- :rate_limit
651
+ :rate_limited
598
652
  when 401, 403
599
- :authentication
653
+ :auth_expired
600
654
  when 500..599
601
- :server_error
655
+ :transient
656
+ when 400
657
+ :permanent
602
658
  else
603
- :network_error
659
+ :transient
604
660
  end
605
661
  when SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
606
- :network_error
607
- when StandardError
608
- # Check error message for common patterns
609
- message = error.message.downcase
610
-
611
- if message.include?("rate limit") || message.include?("quota")
612
- :rate_limit
613
- elsif message.include?("timeout")
614
- :timeout
615
- elsif message.include?("auth") || message.include?("permission")
616
- :authentication
617
- elsif message.include?("server") || message.include?("internal")
618
- :server_error
619
- else
620
- :default
621
- end
662
+ :transient
622
663
  else
623
- :default
664
+ # Use message-based classification from ErrorTaxonomy
665
+ category
624
666
  end
625
667
  end
626
668
  end
@@ -629,22 +671,41 @@ module Aidp
629
671
  def create_recovery_plan(error_info, _context = {})
630
672
  error_type = error_info[:error_type]
631
673
 
674
+ # Use ErrorTaxonomy to determine recovery strategy
632
675
  case error_type
633
- when :rate_limit
676
+ when :rate_limited
634
677
  {
635
678
  action: :switch_provider,
636
679
  reason: "Rate limit reached, switching provider",
637
680
  priority: :high
638
681
  }
639
- when :authentication, :permission_denied
640
- # Previously we escalated immediately. Instead, attempt a provider switch
641
- # so workflows can continue with alternate providers (e.g., Gemini, Cursor)
642
- # while the user resolves credentials for the failing provider.
682
+ when :auth_expired
683
+ # Attempt a provider switch so workflows can continue with alternate providers
684
+ # while the user resolves credentials for the failing provider
643
685
  {
644
686
  action: :switch_provider,
645
- reason: "Authentication/permission issue – switching provider to continue",
687
+ reason: "Authentication expired – switching provider to continue",
646
688
  priority: :critical
647
689
  }
690
+ when :quota_exceeded
691
+ {
692
+ action: :switch_provider,
693
+ reason: "Quota exceeded, switching provider",
694
+ priority: :high
695
+ }
696
+ when :transient
697
+ {
698
+ action: :switch_model,
699
+ reason: "Transient error, trying alternate model",
700
+ priority: :medium
701
+ }
702
+ when :permanent
703
+ {
704
+ action: :escalate,
705
+ reason: "Permanent error, requires manual intervention",
706
+ priority: :critical
707
+ }
708
+ # Legacy error type mappings for backward compatibility
648
709
  when :timeout
649
710
  {
650
711
  action: :switch_model,
@@ -655,7 +716,7 @@ module Aidp
655
716
  {
656
717
  action: :switch_provider,
657
718
  reason: "Network error, switching provider",
658
- priority: :high
719
+ priority: :medium
659
720
  }
660
721
  when :server_error
661
722
  {
@@ -663,6 +724,18 @@ module Aidp
663
724
  reason: "Server error, switching provider",
664
725
  priority: :medium
665
726
  }
727
+ when :authentication, :permission_denied
728
+ {
729
+ action: :switch_provider,
730
+ reason: "Authentication/permission issue – switching provider to continue",
731
+ priority: :critical
732
+ }
733
+ when :rate_limit
734
+ {
735
+ action: :switch_provider,
736
+ reason: "Rate limit reached, switching provider",
737
+ priority: :high
738
+ }
666
739
  else
667
740
  {
668
741
  action: :switch_provider,