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.
- checksums.yaml +4 -4
- data/README.md +14 -14
- data/lib/aidp/agent_harness_adapter.rb +309 -0
- data/lib/aidp/cli/issue_importer.rb +20 -6
- data/lib/aidp/cli/jobs_command.rb +1 -1
- data/lib/aidp/cli/models_command.rb +9 -16
- data/lib/aidp/cli/prompts_command.rb +253 -0
- data/lib/aidp/cli/security_command.rb +4 -10
- data/lib/aidp/cli/storage_command.rb +277 -0
- data/lib/aidp/cli/temporal_command.rb +460 -0
- data/lib/aidp/cli.rb +24 -2
- data/lib/aidp/config.rb +15 -0
- data/lib/aidp/config_paths.rb +12 -2
- data/lib/aidp/database/migrations.rb +114 -0
- data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
- data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
- data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
- data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
- data/lib/aidp/database/repositories/job_repository.rb +278 -0
- data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
- data/lib/aidp/database/repositories/progress_repository.rb +210 -0
- data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
- data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
- data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
- data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
- data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
- data/lib/aidp/database/repositories/task_repository.rb +231 -0
- data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
- data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
- data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
- data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
- data/lib/aidp/database/repository.rb +166 -0
- data/lib/aidp/database/schema.rb +347 -0
- data/lib/aidp/database/storage_migrator.rb +693 -0
- data/lib/aidp/database.rb +139 -0
- data/lib/aidp/debug_mixin.rb +2 -2
- data/lib/aidp/execute/checkpoint.rb +26 -0
- data/lib/aidp/execute/checkpoint_display.rb +16 -0
- data/lib/aidp/execute/prompt_evaluator.rb +30 -57
- data/lib/aidp/execute/work_loop_runner.rb +294 -46
- data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
- data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
- data/lib/aidp/harness/ai_decision_engine.rb +78 -296
- data/lib/aidp/harness/config_schema.rb +59 -0
- data/lib/aidp/harness/configuration.rb +229 -10
- data/lib/aidp/harness/deprecation_cache.rb +53 -11
- data/lib/aidp/harness/enhanced_runner.rb +1 -1
- data/lib/aidp/harness/error_handler.rb +11 -5
- data/lib/aidp/harness/provider_config.rb +36 -0
- data/lib/aidp/harness/provider_factory.rb +26 -24
- data/lib/aidp/harness/provider_info.rb +9 -5
- data/lib/aidp/harness/provider_manager.rb +2 -2
- data/lib/aidp/harness/provider_metrics.rb +103 -1
- data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
- data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
- data/lib/aidp/harness/runner.rb +7 -2
- data/lib/aidp/harness/state_manager.rb +1 -1
- data/lib/aidp/harness/test_runner.rb +167 -11
- data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
- data/lib/aidp/harness/ui.rb +11 -0
- data/lib/aidp/harness/usage_limit.rb +179 -0
- data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
- data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
- data/lib/aidp/harness/usage_period.rb +234 -0
- data/lib/aidp/init/agent_instructions_generator.rb +317 -0
- data/lib/aidp/init/runner.rb +25 -4
- data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
- data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
- data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
- data/lib/aidp/interfaces/logger_interface.rb +150 -0
- data/lib/aidp/interfaces/ui_interface.rb +254 -0
- data/lib/aidp/logger.rb +25 -7
- data/lib/aidp/orchestration_adapter.rb +279 -0
- data/lib/aidp/pr_worktree_manager.rb +4 -15
- data/lib/aidp/prompts/feedback_collector.rb +198 -0
- data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
- data/lib/aidp/prompts/template_evolver.rb +305 -0
- data/lib/aidp/prompts/template_version_manager.rb +325 -0
- data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
- data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
- data/lib/aidp/setup/wizard.rb +616 -176
- data/lib/aidp/shell_executor.rb +58 -0
- data/lib/aidp/style_guide/selector.rb +56 -1
- data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
- data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
- data/lib/aidp/temporal/activities/base_activity.rb +78 -0
- data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
- data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
- data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
- data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
- data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
- data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
- data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
- data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
- data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
- data/lib/aidp/temporal/configuration.rb +169 -0
- data/lib/aidp/temporal/connection.rb +93 -0
- data/lib/aidp/temporal/worker.rb +119 -0
- data/lib/aidp/temporal/workflow_client.rb +171 -0
- data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
- data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
- data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
- data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
- data/lib/aidp/temporal.rb +120 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/base_processor.rb +35 -0
- data/lib/aidp/watch/build_processor.rb +132 -0
- data/lib/aidp/watch/github_state_extractor.rb +0 -8
- data/lib/aidp/watch/rebase_processor.rb +245 -0
- data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
- data/lib/aidp/watch/runner.rb +366 -285
- data/lib/aidp/watch/state_store.rb +80 -0
- data/lib/aidp/watch/work_item.rb +96 -0
- data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
- data/lib/aidp/watch/worktree_reconciler.rb +643 -0
- data/lib/aidp/workflows/guided_agent.rb +1 -1
- data/lib/aidp/worktree.rb +62 -0
- data/templates/aidp.yml.example +70 -0
- metadata +118 -16
- data/lib/aidp/analyze/json_file_storage.rb +0 -292
- data/lib/aidp/execute/future_work_backlog.rb +0 -411
- data/lib/aidp/providers/adapter.rb +0 -239
- data/lib/aidp/providers/aider.rb +0 -264
- data/lib/aidp/providers/anthropic.rb +0 -614
- data/lib/aidp/providers/base.rb +0 -617
- data/lib/aidp/providers/capability_registry.rb +0 -204
- data/lib/aidp/providers/codex.rb +0 -267
- data/lib/aidp/providers/cursor.rb +0 -304
- data/lib/aidp/providers/error_taxonomy.rb +0 -195
- data/lib/aidp/providers/gemini.rb +0 -110
- data/lib/aidp/providers/github_copilot.rb +0 -227
- data/lib/aidp/providers/kilocode.rb +0 -157
- 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
|