aidp 0.34.0 → 0.39.3

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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/aidp/agent_harness_adapter.rb +309 -0
  4. data/lib/aidp/cli/issue_importer.rb +20 -6
  5. data/lib/aidp/cli/jobs_command.rb +1 -1
  6. data/lib/aidp/cli/models_command.rb +9 -16
  7. data/lib/aidp/cli/prompts_command.rb +253 -0
  8. data/lib/aidp/cli/security_command.rb +4 -10
  9. data/lib/aidp/cli/storage_command.rb +277 -0
  10. data/lib/aidp/cli/temporal_command.rb +460 -0
  11. data/lib/aidp/cli.rb +24 -2
  12. data/lib/aidp/config.rb +15 -0
  13. data/lib/aidp/config_paths.rb +12 -2
  14. data/lib/aidp/database/migrations.rb +114 -0
  15. data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
  16. data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
  17. data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
  18. data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
  19. data/lib/aidp/database/repositories/job_repository.rb +278 -0
  20. data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
  21. data/lib/aidp/database/repositories/progress_repository.rb +210 -0
  22. data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
  23. data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
  24. data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
  25. data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
  26. data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
  27. data/lib/aidp/database/repositories/task_repository.rb +231 -0
  28. data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
  29. data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
  30. data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
  31. data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
  32. data/lib/aidp/database/repository.rb +166 -0
  33. data/lib/aidp/database/schema.rb +347 -0
  34. data/lib/aidp/database/storage_migrator.rb +693 -0
  35. data/lib/aidp/database.rb +139 -0
  36. data/lib/aidp/debug_mixin.rb +2 -2
  37. data/lib/aidp/execute/checkpoint.rb +26 -0
  38. data/lib/aidp/execute/checkpoint_display.rb +16 -0
  39. data/lib/aidp/execute/prompt_evaluator.rb +30 -57
  40. data/lib/aidp/execute/work_loop_runner.rb +294 -46
  41. data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
  42. data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
  43. data/lib/aidp/harness/ai_decision_engine.rb +78 -296
  44. data/lib/aidp/harness/config_schema.rb +59 -0
  45. data/lib/aidp/harness/configuration.rb +229 -10
  46. data/lib/aidp/harness/deprecation_cache.rb +53 -11
  47. data/lib/aidp/harness/enhanced_runner.rb +1 -1
  48. data/lib/aidp/harness/error_handler.rb +11 -5
  49. data/lib/aidp/harness/provider_config.rb +36 -0
  50. data/lib/aidp/harness/provider_factory.rb +26 -24
  51. data/lib/aidp/harness/provider_info.rb +9 -5
  52. data/lib/aidp/harness/provider_manager.rb +2 -2
  53. data/lib/aidp/harness/provider_metrics.rb +103 -1
  54. data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
  55. data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
  56. data/lib/aidp/harness/runner.rb +7 -2
  57. data/lib/aidp/harness/state_manager.rb +1 -1
  58. data/lib/aidp/harness/test_runner.rb +167 -11
  59. data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
  60. data/lib/aidp/harness/ui.rb +11 -0
  61. data/lib/aidp/harness/usage_limit.rb +179 -0
  62. data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
  63. data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
  64. data/lib/aidp/harness/usage_period.rb +234 -0
  65. data/lib/aidp/init/agent_instructions_generator.rb +317 -0
  66. data/lib/aidp/init/runner.rb +25 -4
  67. data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
  68. data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
  69. data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
  70. data/lib/aidp/interfaces/logger_interface.rb +150 -0
  71. data/lib/aidp/interfaces/ui_interface.rb +254 -0
  72. data/lib/aidp/logger.rb +25 -7
  73. data/lib/aidp/orchestration_adapter.rb +279 -0
  74. data/lib/aidp/pr_worktree_manager.rb +4 -15
  75. data/lib/aidp/prompts/feedback_collector.rb +198 -0
  76. data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
  77. data/lib/aidp/prompts/template_evolver.rb +305 -0
  78. data/lib/aidp/prompts/template_version_manager.rb +325 -0
  79. data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
  80. data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
  81. data/lib/aidp/setup/wizard.rb +616 -176
  82. data/lib/aidp/shell_executor.rb +58 -0
  83. data/lib/aidp/style_guide/selector.rb +56 -1
  84. data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
  85. data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
  86. data/lib/aidp/temporal/activities/base_activity.rb +78 -0
  87. data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
  88. data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
  89. data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
  90. data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
  91. data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
  92. data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
  93. data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
  94. data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
  95. data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
  96. data/lib/aidp/temporal/configuration.rb +169 -0
  97. data/lib/aidp/temporal/connection.rb +93 -0
  98. data/lib/aidp/temporal/worker.rb +119 -0
  99. data/lib/aidp/temporal/workflow_client.rb +171 -0
  100. data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
  101. data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
  102. data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
  103. data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
  104. data/lib/aidp/temporal.rb +120 -0
  105. data/lib/aidp/version.rb +1 -1
  106. data/lib/aidp/watch/base_processor.rb +35 -0
  107. data/lib/aidp/watch/build_processor.rb +132 -0
  108. data/lib/aidp/watch/github_state_extractor.rb +0 -8
  109. data/lib/aidp/watch/rebase_processor.rb +245 -0
  110. data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
  111. data/lib/aidp/watch/runner.rb +366 -285
  112. data/lib/aidp/watch/state_store.rb +80 -0
  113. data/lib/aidp/watch/work_item.rb +96 -0
  114. data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
  115. data/lib/aidp/watch/worktree_reconciler.rb +643 -0
  116. data/lib/aidp/workflows/guided_agent.rb +1 -1
  117. data/lib/aidp/worktree.rb +62 -0
  118. data/templates/aidp.yml.example +70 -0
  119. metadata +118 -16
  120. data/lib/aidp/analyze/json_file_storage.rb +0 -292
  121. data/lib/aidp/execute/future_work_backlog.rb +0 -411
  122. data/lib/aidp/providers/adapter.rb +0 -239
  123. data/lib/aidp/providers/aider.rb +0 -264
  124. data/lib/aidp/providers/anthropic.rb +0 -614
  125. data/lib/aidp/providers/base.rb +0 -617
  126. data/lib/aidp/providers/capability_registry.rb +0 -204
  127. data/lib/aidp/providers/codex.rb +0 -267
  128. data/lib/aidp/providers/cursor.rb +0 -304
  129. data/lib/aidp/providers/error_taxonomy.rb +0 -195
  130. data/lib/aidp/providers/gemini.rb +0 -110
  131. data/lib/aidp/providers/github_copilot.rb +0 -227
  132. data/lib/aidp/providers/kilocode.rb +0 -157
  133. data/lib/aidp/providers/opencode.rb +0 -139
@@ -171,29 +171,50 @@ module Aidp
171
171
  work_loop_config.fetch(:task_completion_required, true)
172
172
  end
173
173
 
174
- # Get test commands
174
+ # Get all generic deterministic commands
175
+ # Returns array of command configs with normalized structure:
176
+ # [{name:, command:, required:, run_after:, category:, timeout_seconds:}]
177
+ def commands
178
+ @commands ||= load_commands
179
+ end
180
+
181
+ # Get commands filtered by execution phase
182
+ # @param phase [Symbol] :each_unit, :full_loop, or :on_completion
183
+ # @return [Array<Hash>] Commands configured for the specified phase
184
+ def commands_for_phase(phase)
185
+ commands.select { |cmd| cmd[:run_after] == phase.to_sym }
186
+ end
187
+
188
+ # Get commands filtered by category
189
+ # @param category [Symbol] :test, :lint, :formatter, :build, :documentation, etc.
190
+ # @return [Array<Hash>] Commands in the specified category
191
+ def commands_for_category(category)
192
+ commands.select { |cmd| cmd[:category] == category.to_sym }
193
+ end
194
+
195
+ # Get test commands (filtered from generic commands for backwards compatibility)
175
196
  def test_commands
176
- normalize_commands(work_loop_config[:test_commands] || [])
197
+ commands_for_category(:test).map { |cmd| {command: cmd[:command], required: cmd[:required]} }
177
198
  end
178
199
 
179
- # Get lint commands
200
+ # Get lint commands (filtered from generic commands for backwards compatibility)
180
201
  def lint_commands
181
- normalize_commands(work_loop_config[:lint_commands] || [])
202
+ commands_for_category(:lint).map { |cmd| {command: cmd[:command], required: cmd[:required]} }
182
203
  end
183
204
 
184
- # Get formatter commands
205
+ # Get formatter commands (filtered from generic commands for backwards compatibility)
185
206
  def formatter_commands
186
- normalize_commands(work_loop_config[:formatter_commands] || [])
207
+ commands_for_category(:formatter).map { |cmd| {command: cmd[:command], required: cmd[:required]} }
187
208
  end
188
209
 
189
- # Get build commands
210
+ # Get build commands (filtered from generic commands for backwards compatibility)
190
211
  def build_commands
191
- normalize_commands(work_loop_config[:build_commands] || [])
212
+ commands_for_category(:build).map { |cmd| {command: cmd[:command], required: cmd[:required]} }
192
213
  end
193
214
 
194
- # Get documentation commands
215
+ # Get documentation commands (filtered from generic commands for backwards compatibility)
195
216
  def documentation_commands
196
- normalize_commands(work_loop_config[:documentation_commands] || [])
217
+ commands_for_category(:documentation).map { |cmd| {command: cmd[:command], required: cmd[:required]} }
197
218
  end
198
219
 
199
220
  # Get output filtering configuration
@@ -440,6 +461,34 @@ module Aidp
440
461
  thinking_config[:max_tier] || default_thinking_config[:max_tier]
441
462
  end
442
463
 
464
+ # Get maximum tier for autonomous contexts (watch mode, work loops)
465
+ # Restricts escalation to standard tier by default to prevent cost explosion
466
+ # Issue #375: Thinking tiers escalate too quickly in autonomous operations
467
+ def autonomous_max_tier
468
+ thinking_config[:autonomous_max_tier] || default_thinking_config[:autonomous_max_tier]
469
+ end
470
+
471
+ # Get escalation configuration for autonomous contexts
472
+ # Controls how models are tried before tier escalation
473
+ def autonomous_escalation_config
474
+ thinking_config[:autonomous_escalation] || default_autonomous_escalation_config
475
+ end
476
+
477
+ # Minimum attempts per model before trying next model in tier
478
+ def min_attempts_per_model
479
+ autonomous_escalation_config[:min_attempts_per_model] || 2
480
+ end
481
+
482
+ # Minimum total attempts across all models before tier escalation
483
+ def min_total_attempts_before_escalation
484
+ autonomous_escalation_config[:min_total_attempts] || 10
485
+ end
486
+
487
+ # Whether to retry failed models after exhausting untested models
488
+ def retry_failed_models?
489
+ autonomous_escalation_config[:retry_failed_models] != false
490
+ end
491
+
443
492
  # Check if provider switching for tier is allowed
444
493
  def allow_provider_switch_for_tier?
445
494
  thinking_config[:allow_provider_switch] != false
@@ -857,6 +906,157 @@ module Aidp
857
906
  end
858
907
  end
859
908
 
909
+ # Load commands from configuration, supporting both new generic format
910
+ # and legacy category-specific format for backwards compatibility
911
+ def load_commands
912
+ commands = []
913
+
914
+ # Load from new generic commands array if present
915
+ raw_commands = work_loop_config[:commands] || []
916
+ commands.concat(normalize_generic_commands(raw_commands))
917
+
918
+ # Load from legacy category-specific arrays for backwards compatibility
919
+ commands.concat(load_legacy_commands)
920
+
921
+ Aidp.log_debug("configuration", "loaded_commands",
922
+ total: commands.size,
923
+ from_generic: raw_commands.size,
924
+ from_legacy: commands.size - raw_commands.size)
925
+
926
+ commands
927
+ end
928
+
929
+ # Normalize generic command format
930
+ # @param commands [Array] Raw command configurations
931
+ # @return [Array<Hash>] Normalized command configs
932
+ def normalize_generic_commands(commands)
933
+ return [] if commands.nil? || commands.empty?
934
+
935
+ commands.each_with_index.map do |cmd, index|
936
+ case cmd
937
+ when String
938
+ {
939
+ name: "command_#{index}",
940
+ command: cmd,
941
+ required: true,
942
+ run_after: :each_unit,
943
+ category: :custom,
944
+ timeout_seconds: nil
945
+ }
946
+ when Hash
947
+ normalize_generic_command_hash(cmd, index)
948
+ else
949
+ raise ConfigurationError, "Command must be a string or hash, got: #{cmd.class}"
950
+ end
951
+ end
952
+ end
953
+
954
+ # Normalize a single generic command hash
955
+ def normalize_generic_command_hash(cmd, index)
956
+ command_value = cmd[:command] || cmd["command"]
957
+ unless command_value.is_a?(String) && !command_value.empty?
958
+ raise ConfigurationError, "Command must be a non-empty string, got: #{command_value.inspect}"
959
+ end
960
+
961
+ name = cmd[:name] || cmd["name"] || "command_#{index}"
962
+ required = fetch_boolean(cmd, :required, true)
963
+ run_after_raw = cmd[:run_after] || cmd["run_after"] || "each_unit"
964
+ run_after = normalize_run_after(run_after_raw)
965
+ category_raw = cmd[:category] || cmd["category"] || "custom"
966
+ category = category_raw.to_s.to_sym
967
+ timeout = cmd[:timeout_seconds] || cmd["timeout_seconds"]
968
+
969
+ {
970
+ name: name.to_s,
971
+ command: command_value,
972
+ required: required,
973
+ run_after: run_after,
974
+ category: category,
975
+ timeout_seconds: timeout
976
+ }
977
+ end
978
+
979
+ # Normalize run_after value to symbol
980
+ def normalize_run_after(value)
981
+ case value.to_s.downcase
982
+ when "each_unit", "each"
983
+ :each_unit
984
+ when "full_loop", "loop"
985
+ :full_loop
986
+ when "on_completion", "completion"
987
+ :on_completion
988
+ else
989
+ :each_unit
990
+ end
991
+ end
992
+
993
+ # Fetch boolean value from hash with symbol/string key support
994
+ def fetch_boolean(hash, key, default)
995
+ if hash.key?(key)
996
+ hash[key]
997
+ elsif hash.key?(key.to_s)
998
+ hash[key.to_s]
999
+ else
1000
+ default
1001
+ end
1002
+ end
1003
+
1004
+ # Load commands from legacy category-specific configuration
1005
+ # Converts old test_commands, lint_commands, etc. to generic format
1006
+ def load_legacy_commands
1007
+ commands = []
1008
+
1009
+ # Map legacy config keys to categories and default run_after
1010
+ legacy_mappings = {
1011
+ test_commands: {category: :test, run_after: :each_unit},
1012
+ lint_commands: {category: :lint, run_after: :each_unit},
1013
+ formatter_commands: {category: :formatter, run_after: :on_completion},
1014
+ build_commands: {category: :build, run_after: :each_unit},
1015
+ documentation_commands: {category: :documentation, run_after: :on_completion}
1016
+ }
1017
+
1018
+ legacy_mappings.each do |config_key, defaults|
1019
+ raw = work_loop_config[config_key] || []
1020
+ next if raw.empty?
1021
+
1022
+ raw.each_with_index do |cmd, index|
1023
+ normalized = normalize_legacy_command(cmd, defaults[:category], index)
1024
+ normalized[:run_after] = defaults[:run_after]
1025
+ commands << normalized
1026
+ end
1027
+ end
1028
+
1029
+ commands
1030
+ end
1031
+
1032
+ # Normalize a legacy command to generic format
1033
+ def normalize_legacy_command(cmd, category, index)
1034
+ case cmd
1035
+ when String
1036
+ {
1037
+ name: "#{category}_#{index}",
1038
+ command: cmd,
1039
+ required: true,
1040
+ run_after: :each_unit,
1041
+ category: category,
1042
+ timeout_seconds: nil
1043
+ }
1044
+ when Hash
1045
+ command_value = cmd[:command] || cmd["command"]
1046
+ required = fetch_boolean(cmd, :required, true)
1047
+ {
1048
+ name: cmd[:name] || cmd["name"] || "#{category}_#{index}",
1049
+ command: command_value,
1050
+ required: required,
1051
+ run_after: :each_unit,
1052
+ category: category,
1053
+ timeout_seconds: cmd[:timeout_seconds] || cmd["timeout_seconds"]
1054
+ }
1055
+ else
1056
+ raise ConfigurationError, "Legacy command must be a string or hash, got: #{cmd.class}"
1057
+ end
1058
+ end
1059
+
860
1060
  def validate_configuration!
861
1061
  errors = Aidp::Config.validate_harness_config(@config, @project_dir)
862
1062
 
@@ -999,8 +1199,15 @@ module Aidp
999
1199
  {
1000
1200
  enabled: true,
1001
1201
  max_iterations: 50,
1202
+ # New generic commands array - replaces category-specific arrays
1203
+ # Each command specifies when it runs (each_unit, full_loop, on_completion)
1204
+ commands: [],
1205
+ # Legacy category-specific arrays (for backwards compatibility)
1002
1206
  test_commands: [],
1003
1207
  lint_commands: [],
1208
+ formatter_commands: [],
1209
+ build_commands: [],
1210
+ documentation_commands: [],
1004
1211
  guards: default_guards_config,
1005
1212
  units: default_units_config
1006
1213
  }
@@ -1114,15 +1321,27 @@ module Aidp
1114
1321
  {
1115
1322
  default_tier: "mini", # Use mini tier by default for cost optimization
1116
1323
  max_tier: "pro", # Max tier rarely needed; pro is sufficient for most tasks
1324
+ autonomous_max_tier: "standard", # Issue #375: restrict autonomous operations to standard tier
1117
1325
  allow_provider_switch: true,
1118
1326
  auto_escalate: true,
1119
1327
  escalation_threshold: 2,
1120
1328
  escalation: default_escalation_config,
1329
+ autonomous_escalation: default_autonomous_escalation_config,
1121
1330
  permissions_by_tier: {},
1122
1331
  overrides: {}
1123
1332
  }
1124
1333
  end
1125
1334
 
1335
+ # Default autonomous escalation configuration (issue #375)
1336
+ # Controls tier escalation in watch mode and work loops
1337
+ def default_autonomous_escalation_config
1338
+ {
1339
+ min_attempts_per_model: 2, # Each model needs min 2 attempts before trying next
1340
+ min_total_attempts: 10, # Min 10 total attempts before tier escalation
1341
+ retry_failed_models: true # Retry failed models after exhausting untested
1342
+ }
1343
+ end
1344
+
1126
1345
  def default_escalation_config
1127
1346
  {
1128
1347
  on_fail_attempts: 2,
@@ -8,15 +8,31 @@ module Aidp
8
8
  # Manages a dynamic cache of deprecated models detected at runtime
9
9
  # When deprecation errors are detected from provider APIs, models are
10
10
  # added to this cache with metadata (replacement, detected date, etc.)
11
+ #
12
+ # This class is designed to be extractable to the agent-harness gem.
13
+ # It uses dependency injection for logging to decouple from AIDP.
11
14
  class DeprecationCache
12
15
  class CacheError < StandardError; end
13
16
 
17
+ # Component name for logging
18
+ COMPONENT = "deprecation_cache"
19
+
14
20
  attr_reader :cache_path
15
21
 
16
- def initialize(cache_path: nil, root_dir: nil)
22
+ # Initialize the deprecation cache.
23
+ #
24
+ # All parameters are optional for backward compatibility. Existing callers
25
+ # that don't pass a logger will use AIDP's built-in logging.
26
+ #
27
+ # @param cache_path [String, nil] custom cache file path (optional)
28
+ # @param root_dir [String, nil] root directory for default cache path (optional)
29
+ # @param logger [#log_info, #log_warn, #log_error, nil] logger instance (optional,
30
+ # falls back to Aidp.log_* methods if nil)
31
+ def initialize(cache_path: nil, root_dir: nil, logger: nil)
17
32
  @root_dir = root_dir || safe_root_dir
18
33
  @cache_path = cache_path || default_cache_path
19
34
  @cache_data = nil
35
+ @logger = logger
20
36
  ensure_cache_directory
21
37
  end
22
38
 
@@ -36,8 +52,7 @@ module Aidp
36
52
  }.compact
37
53
 
38
54
  save_cache
39
- Aidp.log_info("deprecation_cache", "Added deprecated model",
40
- provider: provider, model: model_id, replacement: replacement)
55
+ log_info("added_deprecated_model", provider: provider, model: model_id, replacement: replacement)
41
56
  end
42
57
 
43
58
  # Check if a model is deprecated
@@ -78,8 +93,7 @@ module Aidp
78
93
  @cache_data["providers"].delete(provider) if @cache_data["providers"][provider].empty?
79
94
 
80
95
  save_cache
81
- Aidp.log_info("deprecation_cache", "Removed deprecated model",
82
- provider: provider, model: model_id)
96
+ log_info("removed_deprecated_model", provider: provider, model: model_id)
83
97
  end
84
98
 
85
99
  # Get full deprecation info for a model
@@ -95,7 +109,7 @@ module Aidp
95
109
  def clear!
96
110
  @cache_data = default_cache_structure
97
111
  save_cache
98
- Aidp.log_info("deprecation_cache", "Cleared all deprecations")
112
+ log_info("cleared_all_deprecations")
99
113
  end
100
114
 
101
115
  # Get cache statistics
@@ -145,16 +159,14 @@ module Aidp
145
159
  @cache_data = default_cache_structure
146
160
  end
147
161
  rescue JSON::ParserError => e
148
- Aidp.log_warn("deprecation_cache", "Invalid cache file, resetting",
149
- error: e.message, path: @cache_path)
162
+ log_warn("invalid_cache_file_resetting", error: e.message, path: @cache_path)
150
163
  @cache_data = default_cache_structure
151
164
  end
152
165
 
153
166
  def save_cache
154
167
  File.write(@cache_path, JSON.pretty_generate(@cache_data))
155
168
  rescue => e
156
- Aidp.log_error("deprecation_cache", "Failed to save cache",
157
- error: e.message, path: @cache_path)
169
+ log_error("failed_to_save_cache", error: e.message, path: @cache_path)
158
170
  raise CacheError, "Failed to save deprecation cache: #{e.message}"
159
171
  end
160
172
 
@@ -168,10 +180,40 @@ module Aidp
168
180
 
169
181
  def validate_cache_structure
170
182
  unless @cache_data.is_a?(Hash) && @cache_data["providers"].is_a?(Hash)
171
- Aidp.log_warn("deprecation_cache", "Invalid cache structure, resetting")
183
+ log_warn("invalid_cache_structure_resetting")
172
184
  @cache_data = default_cache_structure
173
185
  end
174
186
  end
187
+
188
+ # Logging helpers with fallback to Aidp.log_* if no logger injected.
189
+ #
190
+ # Design note: We intentionally treat nil and "not provided" the same way.
191
+ # Both mean "use AIDP's built-in logging". If you need to suppress logging
192
+ # entirely, pass a NullLogger that implements LoggerInterface but does nothing.
193
+ # This keeps the interface simple while supporting the gem extraction use case.
194
+ def log_info(message, **metadata)
195
+ if @logger
196
+ @logger.log_info(COMPONENT, message, **metadata)
197
+ else
198
+ Aidp.log_info(COMPONENT, message, **metadata)
199
+ end
200
+ end
201
+
202
+ def log_warn(message, **metadata)
203
+ if @logger
204
+ @logger.log_warn(COMPONENT, message, **metadata)
205
+ else
206
+ Aidp.log_warn(COMPONENT, message, **metadata)
207
+ end
208
+ end
209
+
210
+ def log_error(message, **metadata)
211
+ if @logger
212
+ @logger.log_error(COMPONENT, message, **metadata)
213
+ else
214
+ Aidp.log_error(COMPONENT, message, **metadata)
215
+ end
216
+ end
175
217
  end
176
218
  end
177
219
  end
@@ -476,7 +476,7 @@ module Aidp
476
476
  }
477
477
 
478
478
  # Show backtrace in debug mode only
479
- if ENV["DEBUG"]
479
+ if Aidp.debug_env_enabled?
480
480
  @tui.show_message("Backtrace: #{error.backtrace&.first(3)&.join("\n")}", :error)
481
481
  end
482
482
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "net/http"
4
+ require "agent_harness"
4
5
  require_relative "../debug_mixin"
5
6
  require_relative "../concurrency"
6
- require_relative "../providers/error_taxonomy"
7
7
  require_relative "../errors"
8
8
 
9
9
  module Aidp
@@ -123,7 +123,7 @@ module Aidp
123
123
  error: error,
124
124
  provider: current_provider,
125
125
  model: current_model_safely,
126
- error_type: @error_classifier.classify_error(error)
126
+ error_type: @error_classifier.classify_error(error)[:error_type]
127
127
  }
128
128
 
129
129
  strategy = retry_strategy(error_info[:error_type])
@@ -264,7 +264,7 @@ module Aidp
264
264
 
265
265
  # Use ErrorTaxonomy to determine if error is retryable
266
266
  error_type = error_info[:error_type]
267
- return false unless Aidp::Providers::ErrorTaxonomy.retryable?(error_type)
267
+ return false unless AgentHarness::ErrorTaxonomy.retryable?(error_type)
268
268
 
269
269
  # Check circuit breaker
270
270
  circuit_breaker_key = "#{error_info[:provider]}:#{error_info[:model]}"
@@ -622,7 +622,7 @@ module Aidp
622
622
  message = error.message.to_s
623
623
 
624
624
  # First, use ErrorTaxonomy to classify by message
625
- category = Aidp::Providers::ErrorTaxonomy.classify_message(message)
625
+ category = AgentHarness::ErrorTaxonomy.classify_message(message)
626
626
 
627
627
  # Override with more specific classification based on error type
628
628
  case error
@@ -645,7 +645,13 @@ module Aidp
645
645
  :transient
646
646
  else
647
647
  # Use message-based classification from ErrorTaxonomy
648
- category
648
+ # Map :unknown and :timeout to :transient for backwards compatibility
649
+ case category
650
+ when :unknown, :timeout
651
+ :transient
652
+ else
653
+ category
654
+ end
649
655
  end
650
656
  end
651
657
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "config_manager"
4
4
  require_relative "provider_type_checker"
5
+ require_relative "usage_limit"
5
6
 
6
7
  module Aidp
7
8
  module Harness
@@ -272,6 +273,41 @@ module Aidp
272
273
  }
273
274
  end
274
275
 
276
+ # Get usage limits configuration
277
+ #
278
+ # @param options [Hash] Options for config loading
279
+ # @return [Hash] Raw usage limits configuration hash
280
+ def usage_limits_config(options = {})
281
+ config = config(options)
282
+ limits = config[:usage_limits] || config["usage_limits"] || {}
283
+
284
+ {
285
+ enabled: limits[:enabled] == true,
286
+ period: limits[:period] || limits["period"] || "monthly",
287
+ reset_day: limits[:reset_day] || limits["reset_day"] || 1,
288
+ max_tokens: limits[:max_tokens] || limits["max_tokens"],
289
+ max_cost: limits[:max_cost] || limits["max_cost"],
290
+ tier_limits: limits[:tier_limits] || limits["tier_limits"] || {}
291
+ }
292
+ end
293
+
294
+ # Get usage limits as a UsageLimit value object
295
+ #
296
+ # @param options [Hash] Options for config loading
297
+ # @return [UsageLimit] UsageLimit instance
298
+ def usage_limit(options = {})
299
+ config = usage_limits_config(options)
300
+ UsageLimit.from_config(config)
301
+ end
302
+
303
+ # Check if usage limits are enabled for this provider
304
+ #
305
+ # @param options [Hash] Options for config loading
306
+ # @return [Boolean] True if limits are enabled
307
+ def usage_limits_enabled?(options = {})
308
+ usage_limits_config(options)[:enabled]
309
+ end
310
+
275
311
  # Get provider-specific harness configuration
276
312
  def harness_config(options = {})
277
313
  config = config(options)
@@ -1,34 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "agent_harness"
3
4
  require_relative "provider_config"
4
- require_relative "../providers/base"
5
- require_relative "../providers/cursor"
6
- require_relative "../providers/anthropic"
7
- require_relative "../providers/gemini"
8
- require_relative "../providers/opencode"
9
- require_relative "../providers/kilocode"
10
- require_relative "../providers/github_copilot"
11
- require_relative "../providers/codex"
12
- require_relative "../providers/aider"
13
5
 
14
6
  module Aidp
15
7
  module Harness
16
8
  # Factory for creating configured provider instances
9
+ #
10
+ # Uses AgentHarness providers for all provider instantiation.
17
11
  class ProviderFactory
18
12
  # Expose for testability
19
13
  attr_reader :provider_instances, :provider_configs
20
14
 
21
- PROVIDER_CLASSES = {
22
- "cursor" => Aidp::Providers::Cursor,
23
- "anthropic" => Aidp::Providers::Anthropic,
24
- "claude" => Aidp::Providers::Anthropic,
25
- "gemini" => Aidp::Providers::Gemini,
26
- "opencode" => Aidp::Providers::Opencode,
27
- "kilocode" => Aidp::Providers::Kilocode,
28
- "github_copilot" => Aidp::Providers::GithubCopilot,
29
- "codex" => Aidp::Providers::Codex,
30
- "aider" => Aidp::Providers::Aider
31
- }.freeze
15
+ # Provider class lookup - delegates to AgentHarness registry
16
+ def self.provider_classes
17
+ registry = AgentHarness::Providers::Registry.instance
18
+ registry.all.each_with_object({}) do |name, hash|
19
+ hash[name.to_s] = registry.get(name)
20
+ # Add aliases
21
+ hash["anthropic"] = registry.get(:claude) if name == :claude
22
+ end
23
+ end
24
+
25
+ # For backwards compatibility - expose PROVIDER_CLASSES as a method
26
+ PROVIDER_CLASSES = nil # Deprecated - use provider_classes method
32
27
 
33
28
  def initialize(config_manager = nil)
34
29
  @config_manager = config_manager || ConfigManager.new
@@ -122,19 +117,26 @@ module Aidp
122
117
  @provider_configs[provider_name.to_s] ||= ProviderConfig.new(provider_name, @config_manager)
123
118
  end
124
119
 
125
- # Get provider class
120
+ # Get provider class from AgentHarness registry
126
121
  def provider_class(provider_name)
127
- PROVIDER_CLASSES[provider_name.to_s]
122
+ name = provider_name.to_s
123
+ # Handle anthropic -> claude alias
124
+ name = "claude" if name == "anthropic"
125
+ AgentHarness::Providers::Registry.instance.get(name.to_sym)
126
+ rescue AgentHarness::ConfigurationError
127
+ nil
128
128
  end
129
129
 
130
130
  # Check if provider is supported
131
131
  def provider_supported?(provider_name)
132
- PROVIDER_CLASSES.key?(provider_name.to_s)
132
+ name = provider_name.to_s
133
+ name = "claude" if name == "anthropic"
134
+ AgentHarness::Providers::Registry.instance.registered?(name.to_sym)
133
135
  end
134
136
 
135
137
  # Get supported provider names
136
138
  def supported_providers
137
- PROVIDER_CLASSES.keys
139
+ AgentHarness::Providers::Registry.instance.all.map(&:to_s)
138
140
  end
139
141
 
140
142
  # Get configured provider names
@@ -154,7 +154,7 @@ module Aidp
154
154
  provider_instance.fetch_mcp_servers
155
155
  rescue => e
156
156
  log_rescue(e, component: "provider_info", action: "fetch_mcp_servers", fallback: [], provider: @provider_name)
157
- warn "Failed to fetch MCP servers for #{@provider_name}: #{e.message}" if ENV["AIDP_DEBUG"]
157
+ warn "Failed to fetch MCP servers for #{@provider_name}: #{e.message}" if Aidp.debug_env_enabled?
158
158
  []
159
159
  end
160
160
 
@@ -359,17 +359,21 @@ module Aidp
359
359
  def provider_instance
360
360
  return @provider_instance if @provider_instance
361
361
 
362
- # Load provider factory and get provider class
363
- require_relative "provider_factory"
362
+ # Get provider class from AgentHarness registry
363
+ require "agent_harness"
364
364
 
365
- provider_class = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES[@provider_name]
365
+ name = @provider_name
366
+ name = "claude" if name == "anthropic"
367
+ provider_class = AgentHarness::Providers::Registry.instance.get(name.to_sym)
366
368
  return nil unless provider_class
367
369
 
368
370
  # Create provider instance
369
371
  @provider_instance = provider_class.new
372
+ rescue AgentHarness::ConfigurationError
373
+ nil
370
374
  rescue => e
371
375
  log_rescue(e, component: "provider_info", action: "create_provider_instance", fallback: nil, provider: @provider_name)
372
- warn "Failed to create provider instance for #{@provider_name}: #{e.message}" if ENV["AIDP_DEBUG"]
376
+ warn "Failed to create provider instance for #{@provider_name}: #{e.message}" if Aidp.debug_env_enabled?
373
377
  nil
374
378
  end
375
379
 
@@ -1118,8 +1118,8 @@ module Aidp
1118
1118
  end
1119
1119
  installed = !path.nil?
1120
1120
  when "cursor"
1121
- require_relative "../providers/cursor"
1122
- installed = Aidp::Providers::Cursor.available?
1121
+ require "agent_harness"
1122
+ installed = AgentHarness::Providers::Registry.instance.get(:cursor).available?
1123
1123
  end
1124
1124
  rescue LoadError => e
1125
1125
  log_rescue(e, component: "provider_manager", action: "check_provider_availability", fallback: false, provider: provider_name)