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
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Interfaces
5
+ # BinaryCheckerInterface defines the contract for checking CLI binary availability.
6
+ # This interface allows for dependency injection of different binary checking
7
+ # implementations, facilitating extraction of provider code into standalone gems.
8
+ #
9
+ # @example Implementing the interface
10
+ # class MyBinaryChecker
11
+ # include Aidp::Interfaces::BinaryCheckerInterface
12
+ #
13
+ # def available?(binary_name)
14
+ # system("which", binary_name, out: File::NULL, err: File::NULL)
15
+ # end
16
+ #
17
+ # def path_for(binary_name)
18
+ # `which #{binary_name}`.chomp
19
+ # end
20
+ # end
21
+ #
22
+ # @example Using an injected checker
23
+ # class Provider
24
+ # def initialize(binary_checker: PathBinaryChecker.new)
25
+ # @binary_checker = binary_checker
26
+ # end
27
+ #
28
+ # def available?
29
+ # @binary_checker.available?("claude")
30
+ # end
31
+ # end
32
+ #
33
+ module BinaryCheckerInterface
34
+ # Check if a binary is available in the system PATH.
35
+ #
36
+ # @param binary_name [String] the name of the binary to check (e.g., "claude", "cursor-agent")
37
+ # @return [Boolean] true if the binary is executable and in PATH
38
+ def available?(binary_name)
39
+ raise NotImplementedError, "#{self.class} must implement #available?"
40
+ end
41
+
42
+ # Get the full path to a binary.
43
+ #
44
+ # @param binary_name [String] the name of the binary to find
45
+ # @return [String, nil] the full path to the binary, or nil if not found
46
+ def path_for(binary_name)
47
+ raise NotImplementedError, "#{self.class} must implement #path_for"
48
+ end
49
+ end
50
+
51
+ # NullBinaryChecker always returns false/nil.
52
+ # Useful for testing when no binaries should be considered available.
53
+ #
54
+ # @example Using in tests
55
+ # provider = Provider.new(binary_checker: NullBinaryChecker.new)
56
+ # provider.available? # => false
57
+ #
58
+ class NullBinaryChecker
59
+ include BinaryCheckerInterface
60
+
61
+ def available?(binary_name)
62
+ false
63
+ end
64
+
65
+ def path_for(binary_name)
66
+ nil
67
+ end
68
+ end
69
+
70
+ # StubBinaryChecker returns configurable responses.
71
+ # Useful for testing specific scenarios.
72
+ #
73
+ # @example Stubbing availability
74
+ # checker = StubBinaryChecker.new(available: {"claude" => true, "cursor-agent" => false})
75
+ # checker.available?("claude") # => true
76
+ # checker.available?("cursor-agent") # => false
77
+ # checker.available?("unknown") # => false
78
+ #
79
+ class StubBinaryChecker
80
+ include BinaryCheckerInterface
81
+
82
+ # @param available [Hash<String, Boolean>] map of binary names to availability
83
+ # @param paths [Hash<String, String>] map of binary names to paths
84
+ def initialize(available: {}, paths: {})
85
+ @available = available
86
+ @paths = paths
87
+ end
88
+
89
+ def available?(binary_name)
90
+ @available.fetch(binary_name.to_s, false)
91
+ end
92
+
93
+ def path_for(binary_name)
94
+ @paths[binary_name.to_s]
95
+ end
96
+ end
97
+
98
+ # PathBinaryChecker checks the system PATH for binaries.
99
+ # This is the standard implementation used by AIDP.
100
+ #
101
+ # @example Creating a checker
102
+ # checker = PathBinaryChecker.new
103
+ # checker.available?("ruby") # => true
104
+ # checker.path_for("ruby") # => "/usr/bin/ruby"
105
+ #
106
+ class PathBinaryChecker
107
+ include BinaryCheckerInterface
108
+
109
+ # Check if a binary is available in the system PATH.
110
+ #
111
+ # @param binary_name [String] the name of the binary to check
112
+ # @return [Boolean] true if the binary is executable and in PATH
113
+ def available?(binary_name)
114
+ !path_for(binary_name).nil?
115
+ end
116
+
117
+ # Get the full path to a binary.
118
+ # Searches the PATH environment variable for the binary.
119
+ #
120
+ # @param binary_name [String] the name of the binary to find
121
+ # @return [String, nil] the full path to the binary, or nil if not found
122
+ def path_for(binary_name)
123
+ return nil if binary_name.nil? || binary_name.empty?
124
+
125
+ # Get extensions to check (Windows support)
126
+ extensions = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
127
+
128
+ # Search each PATH directory
129
+ ENV["PATH"].to_s.split(File::PATH_SEPARATOR).each do |dir|
130
+ extensions.each do |ext|
131
+ path = File.join(dir, "#{binary_name}#{ext}")
132
+ return path if File.executable?(path) && !File.directory?(path)
133
+ end
134
+ end
135
+
136
+ nil
137
+ end
138
+ end
139
+
140
+ # CachingBinaryChecker wraps another checker with TTL-based caching.
141
+ # Reduces filesystem checks for frequently-queried binaries.
142
+ #
143
+ # @example Creating a caching checker
144
+ # inner_checker = PathBinaryChecker.new
145
+ # checker = CachingBinaryChecker.new(inner_checker, ttl: 60)
146
+ # checker.available?("ruby") # filesystem check
147
+ # checker.available?("ruby") # cached result
148
+ #
149
+ class CachingBinaryChecker
150
+ include BinaryCheckerInterface
151
+
152
+ # Default cache TTL in seconds (5 minutes)
153
+ DEFAULT_TTL = 300
154
+
155
+ # @param checker [BinaryCheckerInterface] the underlying checker to cache
156
+ # @param ttl [Integer] cache time-to-live in seconds
157
+ def initialize(checker, ttl: DEFAULT_TTL)
158
+ @checker = checker
159
+ @ttl = ttl
160
+ @cache = {}
161
+ @path_cache = {}
162
+ end
163
+
164
+ def available?(binary_name)
165
+ key = binary_name.to_s
166
+ cached = @cache[key]
167
+
168
+ if cached && !expired?(cached[:timestamp])
169
+ return cached[:value]
170
+ end
171
+
172
+ result = @checker.available?(binary_name)
173
+ @cache[key] = {value: result, timestamp: Time.now}
174
+ result
175
+ end
176
+
177
+ def path_for(binary_name)
178
+ key = binary_name.to_s
179
+ cached = @path_cache[key]
180
+
181
+ if cached && !expired?(cached[:timestamp])
182
+ return cached[:value]
183
+ end
184
+
185
+ result = @checker.path_for(binary_name)
186
+ @path_cache[key] = {value: result, timestamp: Time.now}
187
+ result
188
+ end
189
+
190
+ # Clear all cached results.
191
+ # @return [void]
192
+ def clear_cache!
193
+ @cache.clear
194
+ @path_cache.clear
195
+ end
196
+
197
+ # Clear cached result for a specific binary.
198
+ # @param binary_name [String] the binary name to clear
199
+ # @return [void]
200
+ def clear!(binary_name)
201
+ key = binary_name.to_s
202
+ @cache.delete(key)
203
+ @path_cache.delete(key)
204
+ end
205
+
206
+ private
207
+
208
+ def expired?(timestamp)
209
+ Time.now - timestamp > @ttl
210
+ end
211
+ end
212
+
213
+ # AidpBinaryChecker wraps Aidp::Util.which for compatibility.
214
+ # This adapter bridges the BinaryCheckerInterface to AIDP's existing utility.
215
+ #
216
+ # @example Creating an adapter
217
+ # checker = AidpBinaryChecker.new
218
+ # provider = SomeProvider.new(binary_checker: checker)
219
+ #
220
+ class AidpBinaryChecker
221
+ include BinaryCheckerInterface
222
+
223
+ def available?(binary_name)
224
+ !Aidp::Util.which(binary_name).nil?
225
+ end
226
+
227
+ def path_for(binary_name)
228
+ Aidp::Util.which(binary_name)
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Interfaces
5
+ # CommandExecutorInterface defines the contract for executing shell commands.
6
+ # This interface allows for dependency injection of different command execution
7
+ # backends, facilitating extraction of provider code into standalone gems.
8
+ #
9
+ # @example Implementing the interface
10
+ # class MyExecutor
11
+ # include Aidp::Interfaces::CommandExecutorInterface
12
+ #
13
+ # def execute(command, args: [], input: nil, timeout: nil, **options)
14
+ # # Implementation here
15
+ # CommandResult.new(stdout: "output", stderr: "", exit_status: 0)
16
+ # end
17
+ # end
18
+ #
19
+ # @example Using an injected executor
20
+ # class Provider
21
+ # def initialize(executor: Aidp::Interfaces::TtyCommandExecutor.new)
22
+ # @executor = executor
23
+ # end
24
+ #
25
+ # def run_cli(command, args)
26
+ # @executor.execute(command, args: args, timeout: 30)
27
+ # end
28
+ # end
29
+ #
30
+ module CommandExecutorInterface
31
+ # Execute a shell command.
32
+ #
33
+ # @param command [String] the command to execute (e.g., "claude", "cursor-agent")
34
+ # @param args [Array<String>] command-line arguments
35
+ # @param input [String, nil] input to pass to stdin (or file path to read from)
36
+ # @param timeout [Integer, nil] timeout in seconds
37
+ # @param options [Hash] additional options (env vars, working directory, etc.)
38
+ # @return [CommandResult] the result of the command execution
39
+ # @raise [CommandTimeoutError] if the command times out
40
+ # @raise [CommandExecutionError] if the command fails to execute
41
+ def execute(command, args: [], input: nil, timeout: nil, **options)
42
+ raise NotImplementedError, "#{self.class} must implement #execute"
43
+ end
44
+ end
45
+
46
+ # Result object returned by command execution.
47
+ # Immutable value object containing stdout, stderr, and exit status.
48
+ #
49
+ # @example Creating a result
50
+ # result = CommandResult.new(stdout: "Hello", stderr: "", exit_status: 0)
51
+ # result.success? # => true
52
+ # result.out # => "Hello"
53
+ #
54
+ class CommandResult
55
+ attr_reader :stdout, :stderr, :exit_status
56
+
57
+ # @param stdout [String] standard output from the command
58
+ # @param stderr [String] standard error from the command
59
+ # @param exit_status [Integer] exit status code
60
+ def initialize(stdout:, stderr:, exit_status:)
61
+ @stdout = stdout.to_s.freeze
62
+ @stderr = stderr.to_s.freeze
63
+ @exit_status = exit_status.to_i
64
+ freeze
65
+ end
66
+
67
+ # @return [Boolean] true if exit_status is 0
68
+ def success?
69
+ @exit_status.zero?
70
+ end
71
+
72
+ # Alias for stdout for compatibility with TTY::Command::Result
73
+ # @return [String]
74
+ def out
75
+ @stdout
76
+ end
77
+
78
+ # Alias for stderr for compatibility with TTY::Command::Result
79
+ # @return [String]
80
+ def err
81
+ @stderr
82
+ end
83
+ end
84
+
85
+ # Error raised when a command times out.
86
+ class CommandTimeoutError < StandardError
87
+ attr_reader :command, :timeout
88
+
89
+ def initialize(command:, timeout:)
90
+ @command = command
91
+ @timeout = timeout
92
+ super("Command '#{command}' timed out after #{timeout} seconds")
93
+ end
94
+ end
95
+
96
+ # Error raised when command execution fails.
97
+ class CommandExecutionError < StandardError
98
+ attr_reader :command, :original_error
99
+
100
+ def initialize(command:, original_error:)
101
+ @command = command
102
+ @original_error = original_error
103
+ super("Command '#{command}' failed: #{original_error.message}")
104
+ end
105
+ end
106
+
107
+ # NullExecutor implements CommandExecutorInterface as a no-op.
108
+ # Returns successful empty results. Useful for testing.
109
+ #
110
+ # @example Using as a test double
111
+ # provider = Provider.new(executor: NullExecutor.new)
112
+ #
113
+ class NullExecutor
114
+ include CommandExecutorInterface
115
+
116
+ def execute(command, args: [], input: nil, timeout: nil, **options)
117
+ CommandResult.new(stdout: "", stderr: "", exit_status: 0)
118
+ end
119
+ end
120
+
121
+ # TtyCommandExecutor wraps TTY::Command for command execution.
122
+ # This adapter provides the standard implementation used by AIDP.
123
+ #
124
+ # @example Creating an executor with a logger
125
+ # logger = AidpLoggerAdapter.new
126
+ # executor = TtyCommandExecutor.new(logger: logger)
127
+ #
128
+ class TtyCommandExecutor
129
+ include CommandExecutorInterface
130
+
131
+ # @param logger [LoggerInterface] optional logger for debug output
132
+ # @param component_name [String] component name for logging
133
+ def initialize(logger: nil, component_name: "command_executor")
134
+ @logger = logger
135
+ @component_name = component_name
136
+ end
137
+
138
+ # Execute a shell command using TTY::Command.
139
+ #
140
+ # @param command [String] the command to execute
141
+ # @param args [Array<String>] command-line arguments
142
+ # @param input [String, nil] input to pass to stdin (or file path to read from)
143
+ # @param timeout [Integer, nil] timeout in seconds
144
+ # @param options [Hash] additional options passed to TTY::Command#run
145
+ # @return [CommandResult] the result of the command execution
146
+ def execute(command, args: [], input: nil, timeout: nil, **options)
147
+ require "tty-command"
148
+
149
+ log_debug("executing_command", command: command, args: args, timeout: timeout)
150
+
151
+ start_time = Time.now
152
+
153
+ begin
154
+ cmd_obj = TTY::Command.new(printer: :null)
155
+
156
+ # Prepare input data
157
+ input_data = resolve_input(input)
158
+
159
+ # Execute command - use run! to get result even on non-zero exit
160
+ result = cmd_obj.run!(command, *args, input: input_data, timeout: timeout, **options)
161
+
162
+ duration = Time.now - start_time
163
+ log_debug("command_completed",
164
+ command: command,
165
+ exit_status: result.exit_status,
166
+ duration: duration.round(2))
167
+
168
+ CommandResult.new(
169
+ stdout: result.out,
170
+ stderr: result.err,
171
+ exit_status: result.exit_status
172
+ )
173
+ rescue TTY::Command::TimeoutExceeded
174
+ duration = Time.now - start_time
175
+ log_debug("command_timeout",
176
+ command: command,
177
+ timeout: timeout,
178
+ duration: duration.round(2))
179
+ raise CommandTimeoutError.new(command: command, timeout: timeout)
180
+ rescue Errno::ENOENT, Errno::EACCES => e
181
+ # Command not found or not executable
182
+ duration = Time.now - start_time
183
+ log_debug("command_not_found",
184
+ command: command,
185
+ error: e.message,
186
+ duration: duration.round(2))
187
+ raise CommandExecutionError.new(command: command, original_error: e)
188
+ rescue => e
189
+ duration = Time.now - start_time
190
+ log_debug("command_failed",
191
+ command: command,
192
+ error: e.message,
193
+ duration: duration.round(2))
194
+ raise CommandExecutionError.new(command: command, original_error: e)
195
+ end
196
+ end
197
+
198
+ private
199
+
200
+ def resolve_input(input)
201
+ return nil unless input
202
+
203
+ if input.is_a?(String) && File.exist?(input)
204
+ log_debug("reading_input_file", path: input)
205
+ File.read(input)
206
+ else
207
+ input
208
+ end
209
+ end
210
+
211
+ def log_debug(message, **metadata)
212
+ @logger&.log_debug(@component_name, message, **metadata)
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Interfaces
5
+ # LoggerInterface defines the contract for logging implementations.
6
+ # This interface allows for dependency injection of different logging
7
+ # backends, facilitating extraction of provider code into standalone gems.
8
+ #
9
+ # All logging methods follow a consistent signature:
10
+ # log_<level>(component, message, **metadata)
11
+ #
12
+ # Where:
13
+ # - component: String identifying the subsystem (e.g., "anthropic_provider")
14
+ # - message: String describing the event (e.g., "rate_limit_detected")
15
+ # - metadata: Optional keyword arguments for structured logging context
16
+ #
17
+ # @example Implementing the interface
18
+ # class MyLogger
19
+ # include Aidp::Interfaces::LoggerInterface
20
+ #
21
+ # def log_debug(component, message, **metadata)
22
+ # puts "[DEBUG] #{component}: #{message} #{metadata}"
23
+ # end
24
+ #
25
+ # def log_info(component, message, **metadata)
26
+ # puts "[INFO] #{component}: #{message} #{metadata}"
27
+ # end
28
+ #
29
+ # def log_warn(component, message, **metadata)
30
+ # puts "[WARN] #{component}: #{message} #{metadata}"
31
+ # end
32
+ #
33
+ # def log_error(component, message, **metadata)
34
+ # puts "[ERROR] #{component}: #{message} #{metadata}"
35
+ # end
36
+ # end
37
+ #
38
+ # @example Using an injected logger
39
+ # class Provider
40
+ # def initialize(logger: Aidp::Interfaces::NullLogger.new)
41
+ # @logger = logger
42
+ # end
43
+ #
44
+ # def perform_action
45
+ # @logger.log_debug("provider", "action_started", action: "perform")
46
+ # end
47
+ # end
48
+ #
49
+ module LoggerInterface
50
+ # Log a debug-level message.
51
+ # Use for detailed diagnostic information useful during development.
52
+ #
53
+ # @param component [String] the subsystem or module name
54
+ # @param message [String] the log message
55
+ # @param metadata [Hash] optional structured key-value pairs
56
+ # @return [void]
57
+ def log_debug(component, message, **metadata)
58
+ raise NotImplementedError, "#{self.class} must implement #log_debug"
59
+ end
60
+
61
+ # Log an info-level message.
62
+ # Use for general operational events that don't indicate problems.
63
+ #
64
+ # @param component [String] the subsystem or module name
65
+ # @param message [String] the log message
66
+ # @param metadata [Hash] optional structured key-value pairs
67
+ # @return [void]
68
+ def log_info(component, message, **metadata)
69
+ raise NotImplementedError, "#{self.class} must implement #log_info"
70
+ end
71
+
72
+ # Log a warning-level message.
73
+ # Use for potentially problematic situations that don't prevent operation.
74
+ #
75
+ # @param component [String] the subsystem or module name
76
+ # @param message [String] the log message
77
+ # @param metadata [Hash] optional structured key-value pairs
78
+ # @return [void]
79
+ def log_warn(component, message, **metadata)
80
+ raise NotImplementedError, "#{self.class} must implement #log_warn"
81
+ end
82
+
83
+ # Log an error-level message.
84
+ # Use for error events that might still allow operation to continue.
85
+ #
86
+ # @param component [String] the subsystem or module name
87
+ # @param message [String] the log message
88
+ # @param metadata [Hash] optional structured key-value pairs
89
+ # @return [void]
90
+ def log_error(component, message, **metadata)
91
+ raise NotImplementedError, "#{self.class} must implement #log_error"
92
+ end
93
+ end
94
+
95
+ # NullLogger implements LoggerInterface as a no-op.
96
+ # Useful as a default when no logging is required.
97
+ #
98
+ # @example Using as a default
99
+ # def initialize(logger: NullLogger.new)
100
+ # @logger = logger
101
+ # end
102
+ #
103
+ class NullLogger
104
+ include LoggerInterface
105
+
106
+ def log_debug(component, message, **metadata)
107
+ # no-op
108
+ end
109
+
110
+ def log_info(component, message, **metadata)
111
+ # no-op
112
+ end
113
+
114
+ def log_warn(component, message, **metadata)
115
+ # no-op
116
+ end
117
+
118
+ def log_error(component, message, **metadata)
119
+ # no-op
120
+ end
121
+ end
122
+
123
+ # AidpLoggerAdapter wraps Aidp's module-level logging methods.
124
+ # This adapter bridges the LoggerInterface to AIDP's existing logging system.
125
+ #
126
+ # @example Creating an adapter
127
+ # logger = AidpLoggerAdapter.new
128
+ # provider = SomeProvider.new(logger: logger)
129
+ #
130
+ class AidpLoggerAdapter
131
+ include LoggerInterface
132
+
133
+ def log_debug(component, message, **metadata)
134
+ Aidp.log_debug(component, message, **metadata)
135
+ end
136
+
137
+ def log_info(component, message, **metadata)
138
+ Aidp.log_info(component, message, **metadata)
139
+ end
140
+
141
+ def log_warn(component, message, **metadata)
142
+ Aidp.log_warn(component, message, **metadata)
143
+ end
144
+
145
+ def log_error(component, message, **metadata)
146
+ Aidp.log_error(component, message, **metadata)
147
+ end
148
+ end
149
+ end
150
+ end