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
|
@@ -1,614 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require_relative "base"
|
|
5
|
-
require_relative "../debug_mixin"
|
|
6
|
-
require_relative "../harness/deprecation_cache"
|
|
7
|
-
|
|
8
|
-
module Aidp
|
|
9
|
-
module Providers
|
|
10
|
-
class Anthropic < Base
|
|
11
|
-
include Aidp::DebugMixin
|
|
12
|
-
|
|
13
|
-
attr_reader :model
|
|
14
|
-
|
|
15
|
-
# Model name pattern for Anthropic Claude models
|
|
16
|
-
MODEL_PATTERN = /^claude-[\d.-]+-(?:opus|sonnet|haiku)(?:-\d{8})?$/i
|
|
17
|
-
|
|
18
|
-
# Get deprecation cache instance (lazy loaded)
|
|
19
|
-
def self.deprecation_cache
|
|
20
|
-
@deprecation_cache ||= Aidp::Harness::DeprecationCache.new
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.available?
|
|
24
|
-
!!Aidp::Util.which("claude")
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Normalize a provider-specific model name to its model family
|
|
28
|
-
#
|
|
29
|
-
# Anthropic uses date-versioned models (e.g., "claude-3-5-sonnet-20241022").
|
|
30
|
-
# This method strips the date suffix to get the family name.
|
|
31
|
-
#
|
|
32
|
-
# @param provider_model_name [String] The versioned model name
|
|
33
|
-
# @return [String] The model family name (e.g., "claude-3-5-sonnet")
|
|
34
|
-
def self.model_family(provider_model_name)
|
|
35
|
-
# Strip date suffix: "claude-3-5-sonnet-20241022" → "claude-3-5-sonnet"
|
|
36
|
-
provider_model_name.sub(/-\d{8}$/, "")
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Convert a model family name to the provider's preferred model name
|
|
40
|
-
#
|
|
41
|
-
# Returns the family name as-is. Users can configure specific versions in aidp.yml.
|
|
42
|
-
#
|
|
43
|
-
# @param family_name [String] The model family name
|
|
44
|
-
# @return [String] The model name (same as family for flexibility)
|
|
45
|
-
def self.provider_model_name(family_name)
|
|
46
|
-
family_name
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Check if this provider supports a given model family
|
|
50
|
-
#
|
|
51
|
-
# @param family_name [String] The model family name
|
|
52
|
-
# @return [Boolean] True if it matches Claude model pattern
|
|
53
|
-
def self.supports_model_family?(family_name)
|
|
54
|
-
MODEL_PATTERN.match?(family_name)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Discover available models from Claude CLI
|
|
58
|
-
#
|
|
59
|
-
# @return [Array<Hash>] Array of discovered models
|
|
60
|
-
def self.discover_models
|
|
61
|
-
return [] unless available?
|
|
62
|
-
|
|
63
|
-
begin
|
|
64
|
-
require "open3"
|
|
65
|
-
output, _, status = Open3.capture3("claude", "models", "list", {timeout: 10})
|
|
66
|
-
return [] unless status.success?
|
|
67
|
-
|
|
68
|
-
parse_models_list(output)
|
|
69
|
-
rescue => e
|
|
70
|
-
Aidp.log_debug("anthropic_provider", "discovery failed", error: e.message)
|
|
71
|
-
[]
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Get firewall requirements for Anthropic provider
|
|
76
|
-
def self.firewall_requirements
|
|
77
|
-
{
|
|
78
|
-
domains: [
|
|
79
|
-
"api.anthropic.com",
|
|
80
|
-
"claude.ai",
|
|
81
|
-
"console.anthropic.com"
|
|
82
|
-
],
|
|
83
|
-
ip_ranges: []
|
|
84
|
-
}
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
class << self
|
|
88
|
-
private
|
|
89
|
-
|
|
90
|
-
def parse_models_list(output)
|
|
91
|
-
return [] if output.nil? || output.empty?
|
|
92
|
-
|
|
93
|
-
models = []
|
|
94
|
-
lines = output.lines.map(&:strip)
|
|
95
|
-
|
|
96
|
-
# Skip header and separator lines
|
|
97
|
-
lines.reject! { |line| line.empty? || line.match?(/^[-=]+$/) || line.match?(/^(Model|Name)/i) }
|
|
98
|
-
|
|
99
|
-
lines.each do |line|
|
|
100
|
-
model_info = parse_model_line(line)
|
|
101
|
-
models << model_info if model_info
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
Aidp.log_info("anthropic_provider", "discovered models", count: models.size)
|
|
105
|
-
models
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def parse_model_line(line)
|
|
109
|
-
# Format 1: Simple list of model names
|
|
110
|
-
if line.match?(/^claude-\d/)
|
|
111
|
-
model_name = line.split.first
|
|
112
|
-
return build_model_info(model_name)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Format 2: Table format with columns
|
|
116
|
-
parts = line.split(/\s{2,}/)
|
|
117
|
-
if parts.size >= 1 && parts[0].match?(/^claude/)
|
|
118
|
-
model_name = parts[0]
|
|
119
|
-
model_name = "#{model_name}-#{parts[1]}" if parts.size > 1 && parts[1].match?(/^\d{8}$/)
|
|
120
|
-
return build_model_info(model_name)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Format 3: JSON-like or key-value pairs
|
|
124
|
-
if line.match?(/name:\s*(.+)/)
|
|
125
|
-
model_name = $1.strip
|
|
126
|
-
return build_model_info(model_name)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
nil
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def build_model_info(model_name)
|
|
133
|
-
family = model_family(model_name)
|
|
134
|
-
tier = classify_tier(model_name)
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
name: model_name,
|
|
138
|
-
family: family,
|
|
139
|
-
tier: tier,
|
|
140
|
-
capabilities: extract_capabilities(model_name),
|
|
141
|
-
context_window: infer_context_window(family),
|
|
142
|
-
provider: "anthropic"
|
|
143
|
-
}
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def classify_tier(model_name)
|
|
147
|
-
name_lower = model_name.downcase
|
|
148
|
-
return "advanced" if name_lower.include?("opus")
|
|
149
|
-
return "mini" if name_lower.include?("haiku")
|
|
150
|
-
return "standard" if name_lower.include?("sonnet")
|
|
151
|
-
"standard"
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def extract_capabilities(model_name)
|
|
155
|
-
capabilities = ["chat", "code"]
|
|
156
|
-
name_lower = model_name.downcase
|
|
157
|
-
capabilities << "vision" unless name_lower.include?("haiku")
|
|
158
|
-
capabilities
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def infer_context_window(family)
|
|
162
|
-
family.match?(/claude-3/) ? 200_000 : nil
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Public instance methods (called from workflows and harness)
|
|
167
|
-
public
|
|
168
|
-
|
|
169
|
-
def name
|
|
170
|
-
"anthropic"
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def display_name
|
|
174
|
-
"Anthropic Claude CLI"
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def supports_mcp?
|
|
178
|
-
true
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def fetch_mcp_servers
|
|
182
|
-
return [] unless self.class.available?
|
|
183
|
-
|
|
184
|
-
begin
|
|
185
|
-
# Use claude mcp list command
|
|
186
|
-
result = debug_execute_command("claude", args: ["mcp", "list"], timeout: 5)
|
|
187
|
-
return [] unless result.exit_status == 0
|
|
188
|
-
|
|
189
|
-
parse_claude_mcp_output(result.out)
|
|
190
|
-
rescue => e
|
|
191
|
-
debug_log("Failed to fetch MCP servers via Claude CLI: #{e.message}", level: :debug)
|
|
192
|
-
[]
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def available?
|
|
197
|
-
self.class.available?
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# ProviderAdapter interface methods
|
|
201
|
-
|
|
202
|
-
def capabilities
|
|
203
|
-
{
|
|
204
|
-
reasoning_tiers: ["mini", "standard", "thinking"],
|
|
205
|
-
context_window: 200_000,
|
|
206
|
-
supports_json_mode: true,
|
|
207
|
-
supports_tool_use: true,
|
|
208
|
-
supports_vision: false,
|
|
209
|
-
supports_file_upload: true
|
|
210
|
-
}
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def supports_dangerous_mode?
|
|
214
|
-
true
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def dangerous_mode_flags
|
|
218
|
-
["--dangerously-skip-permissions"]
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Classify provider error using string matching
|
|
222
|
-
#
|
|
223
|
-
# ZFC EXCEPTION: Cannot use AI to classify provider errors because:
|
|
224
|
-
# 1. The failing provider IS the AI we'd use for classification (circular dependency)
|
|
225
|
-
# 2. Provider may be rate-limited, down, or misconfigured
|
|
226
|
-
# 3. Error classification must work even when AI unavailable
|
|
227
|
-
#
|
|
228
|
-
# This is a legitimate exception to ZFC principles per LLM_STYLE_GUIDE:
|
|
229
|
-
# "Structural safety checks" are allowed in code when AI cannot be used.
|
|
230
|
-
#
|
|
231
|
-
# @param error_message [String] The error message to classify
|
|
232
|
-
# @return [Hash] Classification result with :type, :is_deprecation, :is_rate_limit, :confidence
|
|
233
|
-
def self.classify_provider_error(error_message)
|
|
234
|
-
msg_lower = error_message.downcase
|
|
235
|
-
|
|
236
|
-
# Use simple string.include? checks (not regex) to avoid ReDoS vulnerabilities
|
|
237
|
-
is_rate_limit = msg_lower.include?("rate limit") || msg_lower.include?("session limit")
|
|
238
|
-
is_deprecation = msg_lower.include?("deprecat") || msg_lower.include?("end-of-life")
|
|
239
|
-
is_auth_error = msg_lower.include?("auth") && (msg_lower.include?("expired") || msg_lower.include?("invalid"))
|
|
240
|
-
|
|
241
|
-
# Determine primary type
|
|
242
|
-
type = if is_rate_limit
|
|
243
|
-
"rate_limit"
|
|
244
|
-
elsif is_deprecation
|
|
245
|
-
"deprecation"
|
|
246
|
-
elsif is_auth_error
|
|
247
|
-
"auth_error"
|
|
248
|
-
else
|
|
249
|
-
"other"
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
Aidp.log_debug("anthropic", "Provider error classification",
|
|
253
|
-
type: type,
|
|
254
|
-
is_rate_limit: is_rate_limit,
|
|
255
|
-
is_deprecation: is_deprecation,
|
|
256
|
-
is_auth_error: is_auth_error)
|
|
257
|
-
|
|
258
|
-
{
|
|
259
|
-
type: type,
|
|
260
|
-
is_rate_limit: is_rate_limit,
|
|
261
|
-
is_deprecation: is_deprecation,
|
|
262
|
-
is_auth_error: is_auth_error,
|
|
263
|
-
confidence: 0.85, # Good confidence for clear error messages
|
|
264
|
-
reasoning: "Pattern-based classification (ZFC exception: circular dependency)"
|
|
265
|
-
}
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
# Error patterns for classify_error method (legacy support)
|
|
269
|
-
# NOTE: ZFC-based classification preferred - see classify_error_with_zfc
|
|
270
|
-
# These patterns serve as fallback when ZFC is unavailable
|
|
271
|
-
def error_patterns
|
|
272
|
-
{
|
|
273
|
-
rate_limited: [
|
|
274
|
-
/rate.?limit/i,
|
|
275
|
-
/too.?many.?requests/i,
|
|
276
|
-
/429/,
|
|
277
|
-
/overloaded/i
|
|
278
|
-
],
|
|
279
|
-
auth_expired: [
|
|
280
|
-
/oauth.*token.*expired/i,
|
|
281
|
-
/authentication.*error/i,
|
|
282
|
-
/invalid.*api.*key/i,
|
|
283
|
-
/unauthorized/i,
|
|
284
|
-
/401/
|
|
285
|
-
],
|
|
286
|
-
quota_exceeded: [
|
|
287
|
-
/quota.*exceeded/i,
|
|
288
|
-
/usage.*limit/i,
|
|
289
|
-
/credit.*exhausted/i
|
|
290
|
-
],
|
|
291
|
-
transient: [
|
|
292
|
-
/timeout/i,
|
|
293
|
-
/connection.*reset/i,
|
|
294
|
-
/temporary.*error/i,
|
|
295
|
-
/service.*unavailable/i,
|
|
296
|
-
/503/,
|
|
297
|
-
/502/,
|
|
298
|
-
/504/
|
|
299
|
-
],
|
|
300
|
-
permanent: [
|
|
301
|
-
/invalid.*model/i,
|
|
302
|
-
/unsupported.*operation/i,
|
|
303
|
-
/not.*found/i,
|
|
304
|
-
/404/,
|
|
305
|
-
/bad.*request/i,
|
|
306
|
-
/400/,
|
|
307
|
-
/model.*deprecated/i,
|
|
308
|
-
/end-of-life/i
|
|
309
|
-
]
|
|
310
|
-
}
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
# Check if a model is deprecated and return replacement
|
|
314
|
-
# @param model_name [String] The model name to check
|
|
315
|
-
# @return [String, nil] Replacement model name if deprecated, nil otherwise
|
|
316
|
-
def self.check_model_deprecation(model_name)
|
|
317
|
-
deprecation_cache.replacement_for(provider: "anthropic", model_id: model_name)
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
# Find replacement model for deprecated one using RubyLLM registry
|
|
321
|
-
# @param deprecated_model [String] The deprecated model name
|
|
322
|
-
# @param provider_name [String] Provider name for registry lookup
|
|
323
|
-
# @return [String, nil] Latest model in the same family, or configured replacement
|
|
324
|
-
def self.find_replacement_model(deprecated_model, provider_name: "anthropic")
|
|
325
|
-
# First check the deprecation cache for explicit replacement
|
|
326
|
-
replacement = deprecation_cache.replacement_for(provider: provider_name, model_id: deprecated_model)
|
|
327
|
-
return replacement if replacement
|
|
328
|
-
|
|
329
|
-
# Try to find latest model in same family using registry
|
|
330
|
-
require_relative "../harness/ruby_llm_registry" unless defined?(Aidp::Harness::RubyLLMRegistry)
|
|
331
|
-
|
|
332
|
-
begin
|
|
333
|
-
registry = Aidp::Harness::RubyLLMRegistry.new
|
|
334
|
-
|
|
335
|
-
# Search for non-deprecated models in the same family
|
|
336
|
-
# Prefer models without "latest" suffix, sorted by ID (newer dates first)
|
|
337
|
-
models = registry.models_for_provider(provider_name)
|
|
338
|
-
candidates = models.select { |m| m.include?("sonnet") && !deprecation_cache.deprecated?(provider: provider_name, model_id: m) }
|
|
339
|
-
|
|
340
|
-
# Prioritize: versioned models over -latest, newer versions first
|
|
341
|
-
versioned = candidates.reject { |m| m.end_with?("-latest") }.sort.reverse
|
|
342
|
-
latest_models = candidates.select { |m| m.end_with?("-latest") }
|
|
343
|
-
|
|
344
|
-
replacement = versioned.first || latest_models.first
|
|
345
|
-
|
|
346
|
-
if replacement
|
|
347
|
-
Aidp.log_info("anthropic", "Found replacement model",
|
|
348
|
-
deprecated: deprecated_model,
|
|
349
|
-
replacement: replacement)
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
replacement
|
|
353
|
-
rescue => e
|
|
354
|
-
Aidp.log_error("anthropic", "Failed to find replacement model",
|
|
355
|
-
deprecated: deprecated_model,
|
|
356
|
-
error: e.message)
|
|
357
|
-
nil
|
|
358
|
-
end
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
def send_message(prompt:, session: nil, options: {})
|
|
362
|
-
raise "claude CLI not available" unless self.class.available?
|
|
363
|
-
|
|
364
|
-
# Check if current model is deprecated and warn
|
|
365
|
-
if @model && (replacement = self.class.check_model_deprecation(@model))
|
|
366
|
-
Aidp.log_warn("anthropic", "Using deprecated model",
|
|
367
|
-
current: @model,
|
|
368
|
-
replacement: replacement)
|
|
369
|
-
debug_log("⚠️ Model #{@model} is deprecated. Consider upgrading to #{replacement}", level: :warn)
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
# Smart timeout calculation with tier awareness
|
|
373
|
-
timeout_seconds = calculate_timeout(options)
|
|
374
|
-
|
|
375
|
-
debug_provider("claude", "Starting execution", {timeout: timeout_seconds, tier: options[:tier]})
|
|
376
|
-
debug_log("📝 Sending prompt to claude...", level: :info)
|
|
377
|
-
|
|
378
|
-
# Build command arguments
|
|
379
|
-
args = ["--print", "--output-format=text"]
|
|
380
|
-
|
|
381
|
-
# Add model if specified
|
|
382
|
-
if @model && !@model.empty?
|
|
383
|
-
args << "--model" << @model
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
# Check if we should skip permissions (devcontainer support)
|
|
387
|
-
if should_skip_permissions?
|
|
388
|
-
args << "--dangerously-skip-permissions"
|
|
389
|
-
debug_log("🔓 Running with elevated permissions (devcontainer mode)", level: :info)
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
begin
|
|
393
|
-
result = debug_execute_command("claude", args: args, input: prompt, timeout: timeout_seconds)
|
|
394
|
-
|
|
395
|
-
# Log the results
|
|
396
|
-
debug_command("claude", args: args, input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
|
|
397
|
-
|
|
398
|
-
if result.exit_status == 0
|
|
399
|
-
result.out
|
|
400
|
-
else
|
|
401
|
-
# Detect issues in stdout/stderr (Claude sometimes prints to stdout)
|
|
402
|
-
combined = [result.out, result.err].compact.join("\n")
|
|
403
|
-
|
|
404
|
-
# Classify provider error using pattern matching
|
|
405
|
-
# ZFC EXCEPTION: Cannot use AI to classify provider's own errors (circular dependency)
|
|
406
|
-
error_classification = self.class.classify_provider_error(combined)
|
|
407
|
-
|
|
408
|
-
Aidp.log_debug("anthropic_provider", "error_classified",
|
|
409
|
-
exit_code: result.exit_status,
|
|
410
|
-
type: error_classification[:type],
|
|
411
|
-
confidence: error_classification[:confidence])
|
|
412
|
-
|
|
413
|
-
# Check for model deprecation FIRST (before rate limiting)
|
|
414
|
-
# Even if rate limited, we need to cache the deprecation for next run
|
|
415
|
-
if error_classification[:is_deprecation]
|
|
416
|
-
deprecated_model = @model
|
|
417
|
-
Aidp.log_error("anthropic", "Model deprecation detected",
|
|
418
|
-
model: deprecated_model,
|
|
419
|
-
message: combined)
|
|
420
|
-
|
|
421
|
-
# Try to find replacement
|
|
422
|
-
replacement = deprecated_model ? self.class.find_replacement_model(deprecated_model) : nil
|
|
423
|
-
|
|
424
|
-
# Record deprecation in cache for future runs
|
|
425
|
-
if replacement
|
|
426
|
-
self.class.deprecation_cache.add_deprecated_model(
|
|
427
|
-
provider: "anthropic",
|
|
428
|
-
model_id: deprecated_model,
|
|
429
|
-
replacement: replacement,
|
|
430
|
-
reason: combined.lines.first&.strip || "Model deprecated"
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
Aidp.log_info("anthropic", "Auto-upgrading to non-deprecated model",
|
|
434
|
-
old_model: deprecated_model,
|
|
435
|
-
new_model: replacement)
|
|
436
|
-
debug_log("🔄 Upgrading from deprecated model #{deprecated_model} to #{replacement}", level: :info)
|
|
437
|
-
|
|
438
|
-
# Update model and retry
|
|
439
|
-
@model = replacement
|
|
440
|
-
|
|
441
|
-
# Retry with new model (even if rate limited, we'll hit rate limit with new model)
|
|
442
|
-
debug_log("🔄 Retrying with upgraded model: #{replacement}", level: :info)
|
|
443
|
-
return send_message(prompt: prompt, session: session, options: options)
|
|
444
|
-
else
|
|
445
|
-
# Record deprecation even without replacement
|
|
446
|
-
if deprecated_model
|
|
447
|
-
self.class.deprecation_cache.add_deprecated_model(
|
|
448
|
-
provider: "anthropic",
|
|
449
|
-
model_id: deprecated_model,
|
|
450
|
-
replacement: nil,
|
|
451
|
-
reason: combined.lines.first&.strip || "Model deprecated"
|
|
452
|
-
)
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
error_message = "Model '#{deprecated_model}' is deprecated and no replacement found.\n#{combined}"
|
|
456
|
-
error = RuntimeError.new(error_message)
|
|
457
|
-
debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
458
|
-
raise error
|
|
459
|
-
end
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
# Check for rate limit (after handling deprecation)
|
|
463
|
-
if error_classification[:is_rate_limit]
|
|
464
|
-
Aidp.log_debug("anthropic_provider", "rate_limit_detected",
|
|
465
|
-
exit_code: result.exit_status,
|
|
466
|
-
confidence: error_classification[:confidence],
|
|
467
|
-
message: combined)
|
|
468
|
-
notify_rate_limit(combined)
|
|
469
|
-
error_message = "Rate limit reached for Claude CLI.\n#{combined}"
|
|
470
|
-
error = RuntimeError.new(error_message)
|
|
471
|
-
debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
472
|
-
raise error
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
# Check for auth issues
|
|
476
|
-
if combined.downcase.include?("oauth token has expired") || combined.downcase.include?("authentication_error")
|
|
477
|
-
error_message = "Authentication error from Claude CLI: token expired or invalid.\n" \
|
|
478
|
-
"Run 'claude /login' or refresh credentials.\n" \
|
|
479
|
-
"Note: Model discovery requires valid authentication."
|
|
480
|
-
error = RuntimeError.new(error_message)
|
|
481
|
-
debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
482
|
-
raise error
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
error_message = "claude failed with exit code #{result.exit_status}: #{result.err}"
|
|
486
|
-
error = RuntimeError.new(error_message)
|
|
487
|
-
debug_error(error, {exit_code: result.exit_status, stderr: result.err})
|
|
488
|
-
raise error
|
|
489
|
-
end
|
|
490
|
-
rescue => e
|
|
491
|
-
debug_error(e, {provider: "claude", prompt_length: prompt.length})
|
|
492
|
-
raise
|
|
493
|
-
end
|
|
494
|
-
end
|
|
495
|
-
|
|
496
|
-
private
|
|
497
|
-
|
|
498
|
-
# Notify harness about rate limit detection
|
|
499
|
-
def notify_rate_limit(message)
|
|
500
|
-
return unless @harness_context
|
|
501
|
-
|
|
502
|
-
# Extract reset time from message (e.g., "resets 4am")
|
|
503
|
-
reset_time = extract_reset_time_from_message(message)
|
|
504
|
-
|
|
505
|
-
# Notify provider manager if available
|
|
506
|
-
if @harness_context.respond_to?(:provider_manager)
|
|
507
|
-
provider_manager = @harness_context.provider_manager
|
|
508
|
-
if provider_manager.respond_to?(:mark_rate_limited)
|
|
509
|
-
provider_manager.mark_rate_limited("anthropic", reset_time)
|
|
510
|
-
Aidp.log_debug("anthropic_provider", "notified_provider_manager",
|
|
511
|
-
reset_time: reset_time)
|
|
512
|
-
end
|
|
513
|
-
end
|
|
514
|
-
rescue => e
|
|
515
|
-
Aidp.log_debug("anthropic_provider", "notify_rate_limit_failed",
|
|
516
|
-
error: e.message)
|
|
517
|
-
end
|
|
518
|
-
|
|
519
|
-
# Extract reset time from rate limit message
|
|
520
|
-
def extract_reset_time_from_message(message)
|
|
521
|
-
# Handle expressions like "resets 4am" or "reset at 4:30pm"
|
|
522
|
-
time_of_day_match = message.match(/reset(?:s)?(?:\s+at)?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i)
|
|
523
|
-
if time_of_day_match
|
|
524
|
-
hour = time_of_day_match[1].to_i
|
|
525
|
-
minute = time_of_day_match[2] ? time_of_day_match[2].to_i : 0
|
|
526
|
-
meridiem = time_of_day_match[3].downcase
|
|
527
|
-
|
|
528
|
-
hour %= 12
|
|
529
|
-
hour += 12 if meridiem == "pm"
|
|
530
|
-
|
|
531
|
-
now = Time.now
|
|
532
|
-
reset_time = Time.new(now.year, now.month, now.day, hour, minute, 0, now.utc_offset)
|
|
533
|
-
reset_time += 86_400 if reset_time <= now
|
|
534
|
-
return reset_time
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
# Default to 1 hour from now if no specific time found
|
|
538
|
-
Time.now + 3600
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
# Check if we should skip permissions based on devcontainer configuration
|
|
542
|
-
# Overrides base class to add logging and Claude-specific config check
|
|
543
|
-
def should_skip_permissions?
|
|
544
|
-
# Use base class devcontainer detection
|
|
545
|
-
if in_devcontainer_or_codespace?
|
|
546
|
-
debug_log("🔓 Detected devcontainer/codespace environment - enabling full permissions", level: :info)
|
|
547
|
-
return true
|
|
548
|
-
end
|
|
549
|
-
|
|
550
|
-
# Fallback: Check harness context for Claude-specific configuration
|
|
551
|
-
if @harness_context&.config&.respond_to?(:should_use_full_permissions?)
|
|
552
|
-
return @harness_context.config.should_use_full_permissions?("claude")
|
|
553
|
-
end
|
|
554
|
-
|
|
555
|
-
false
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
# Parse Claude MCP server list output
|
|
559
|
-
def parse_claude_mcp_output(output)
|
|
560
|
-
servers = []
|
|
561
|
-
return servers unless output
|
|
562
|
-
|
|
563
|
-
lines = output.lines
|
|
564
|
-
lines.reject! { |line| /checking mcp server health/i.match?(line) }
|
|
565
|
-
|
|
566
|
-
lines.each do |line|
|
|
567
|
-
line = line.strip
|
|
568
|
-
next if line.empty?
|
|
569
|
-
|
|
570
|
-
# Try to parse Claude format: "name: command - ✓ Connected"
|
|
571
|
-
if line =~ /^([^:]+):\s*(.+?)\s*-\s*(✓|✗)\s*(.+)$/
|
|
572
|
-
name = Regexp.last_match(1).strip
|
|
573
|
-
command = Regexp.last_match(2).strip
|
|
574
|
-
status_symbol = Regexp.last_match(3)
|
|
575
|
-
status_text = Regexp.last_match(4).strip
|
|
576
|
-
|
|
577
|
-
servers << {
|
|
578
|
-
name: name,
|
|
579
|
-
status: (status_symbol == "✓") ? "connected" : "error",
|
|
580
|
-
description: command,
|
|
581
|
-
enabled: status_symbol == "✓",
|
|
582
|
-
error: (status_symbol == "✗") ? status_text : nil,
|
|
583
|
-
source: "claude_cli"
|
|
584
|
-
}
|
|
585
|
-
next
|
|
586
|
-
end
|
|
587
|
-
|
|
588
|
-
# Try to parse legacy table format
|
|
589
|
-
next if /Name.*Status/i.match?(line)
|
|
590
|
-
next if /^[-=]+$/.match?(line)
|
|
591
|
-
|
|
592
|
-
parts = line.split(/\s{2,}/)
|
|
593
|
-
next if parts.size < 2
|
|
594
|
-
|
|
595
|
-
name = parts[0]&.strip
|
|
596
|
-
status = parts[1]&.strip
|
|
597
|
-
description = parts[2..]&.join(" ")&.strip
|
|
598
|
-
|
|
599
|
-
next unless name && !name.empty?
|
|
600
|
-
|
|
601
|
-
servers << {
|
|
602
|
-
name: name,
|
|
603
|
-
status: status || "unknown",
|
|
604
|
-
description: description,
|
|
605
|
-
enabled: status&.downcase == "enabled" || status&.downcase == "connected",
|
|
606
|
-
source: "claude_cli"
|
|
607
|
-
}
|
|
608
|
-
end
|
|
609
|
-
|
|
610
|
-
servers
|
|
611
|
-
end
|
|
612
|
-
end
|
|
613
|
-
end
|
|
614
|
-
end
|