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
@@ -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