aidp 0.26.0 → 0.28.0
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 +89 -0
- data/lib/aidp/cli/checkpoint_command.rb +198 -0
- data/lib/aidp/cli/config_command.rb +71 -0
- data/lib/aidp/cli/enhanced_input.rb +2 -0
- data/lib/aidp/cli/first_run_wizard.rb +8 -7
- data/lib/aidp/cli/harness_command.rb +102 -0
- data/lib/aidp/cli/jobs_command.rb +3 -3
- data/lib/aidp/cli/mcp_dashboard.rb +4 -3
- data/lib/aidp/cli/models_command.rb +661 -0
- data/lib/aidp/cli/providers_command.rb +223 -0
- data/lib/aidp/cli.rb +45 -464
- data/lib/aidp/config.rb +54 -0
- data/lib/aidp/daemon/runner.rb +2 -2
- data/lib/aidp/debug_mixin.rb +25 -10
- data/lib/aidp/execute/agent_signal_parser.rb +22 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
- data/lib/aidp/execute/checkpoint_display.rb +38 -37
- data/lib/aidp/execute/interactive_repl.rb +2 -1
- data/lib/aidp/execute/prompt_manager.rb +4 -4
- data/lib/aidp/execute/repl_macros.rb +2 -2
- data/lib/aidp/execute/steps.rb +94 -1
- data/lib/aidp/execute/work_loop_runner.rb +238 -19
- data/lib/aidp/execute/workflow_selector.rb +4 -27
- data/lib/aidp/firewall/provider_requirements_collector.rb +262 -0
- data/lib/aidp/harness/ai_decision_engine.rb +35 -2
- data/lib/aidp/harness/config_manager.rb +5 -10
- data/lib/aidp/harness/config_schema.rb +8 -0
- data/lib/aidp/harness/configuration.rb +40 -2
- data/lib/aidp/harness/enhanced_runner.rb +25 -19
- data/lib/aidp/harness/error_handler.rb +23 -73
- data/lib/aidp/harness/model_cache.rb +269 -0
- data/lib/aidp/harness/model_discovery_service.rb +259 -0
- data/lib/aidp/harness/model_registry.rb +201 -0
- data/lib/aidp/harness/provider_factory.rb +11 -2
- data/lib/aidp/harness/runner.rb +5 -0
- data/lib/aidp/harness/state_manager.rb +0 -7
- data/lib/aidp/harness/thinking_depth_manager.rb +202 -7
- data/lib/aidp/harness/ui/enhanced_tui.rb +8 -18
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +0 -18
- data/lib/aidp/harness/ui/progress_display.rb +6 -2
- data/lib/aidp/harness/user_interface.rb +0 -58
- data/lib/aidp/init/runner.rb +7 -2
- data/lib/aidp/message_display.rb +0 -46
- data/lib/aidp/planning/analyzers/feedback_analyzer.rb +365 -0
- data/lib/aidp/planning/builders/agile_plan_builder.rb +387 -0
- data/lib/aidp/planning/builders/project_plan_builder.rb +193 -0
- data/lib/aidp/planning/generators/gantt_generator.rb +190 -0
- data/lib/aidp/planning/generators/iteration_plan_generator.rb +392 -0
- data/lib/aidp/planning/generators/legacy_research_planner.rb +473 -0
- data/lib/aidp/planning/generators/marketing_report_generator.rb +348 -0
- data/lib/aidp/planning/generators/mvp_scope_generator.rb +310 -0
- data/lib/aidp/planning/generators/user_test_plan_generator.rb +373 -0
- data/lib/aidp/planning/generators/wbs_generator.rb +259 -0
- data/lib/aidp/planning/mappers/persona_mapper.rb +163 -0
- data/lib/aidp/planning/parsers/document_parser.rb +141 -0
- data/lib/aidp/planning/parsers/feedback_data_parser.rb +252 -0
- data/lib/aidp/provider_manager.rb +8 -32
- data/lib/aidp/providers/adapter.rb +2 -4
- data/lib/aidp/providers/aider.rb +264 -0
- data/lib/aidp/providers/anthropic.rb +206 -121
- data/lib/aidp/providers/base.rb +123 -3
- data/lib/aidp/providers/capability_registry.rb +0 -1
- data/lib/aidp/providers/codex.rb +75 -70
- data/lib/aidp/providers/cursor.rb +87 -59
- data/lib/aidp/providers/gemini.rb +57 -60
- data/lib/aidp/providers/github_copilot.rb +19 -66
- data/lib/aidp/providers/kilocode.rb +35 -80
- data/lib/aidp/providers/opencode.rb +35 -80
- data/lib/aidp/setup/wizard.rb +555 -8
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +211 -30
- data/lib/aidp/watch/change_request_processor.rb +128 -14
- data/lib/aidp/watch/ci_fix_processor.rb +103 -37
- data/lib/aidp/watch/ci_log_extractor.rb +258 -0
- data/lib/aidp/watch/github_state_extractor.rb +177 -0
- data/lib/aidp/watch/implementation_verifier.rb +284 -0
- data/lib/aidp/watch/plan_generator.rb +95 -52
- data/lib/aidp/watch/plan_processor.rb +7 -6
- data/lib/aidp/watch/repository_client.rb +245 -17
- data/lib/aidp/watch/review_processor.rb +100 -19
- data/lib/aidp/watch/reviewers/base_reviewer.rb +1 -1
- data/lib/aidp/watch/runner.rb +181 -29
- data/lib/aidp/watch/state_store.rb +22 -1
- data/lib/aidp/workflows/definitions.rb +147 -0
- data/lib/aidp/workflows/guided_agent.rb +3 -3
- data/lib/aidp/workstream_cleanup.rb +245 -0
- data/lib/aidp/worktree.rb +19 -0
- data/templates/aidp-development.yml.example +2 -2
- data/templates/aidp-production.yml.example +3 -3
- data/templates/aidp.yml.example +57 -0
- data/templates/implementation/generate_tdd_specs.md +213 -0
- data/templates/implementation/iterative_implementation.md +122 -0
- data/templates/planning/agile/analyze_feedback.md +183 -0
- data/templates/planning/agile/generate_iteration_plan.md +179 -0
- data/templates/planning/agile/generate_legacy_research_plan.md +171 -0
- data/templates/planning/agile/generate_marketing_report.md +162 -0
- data/templates/planning/agile/generate_mvp_scope.md +127 -0
- data/templates/planning/agile/generate_user_test_plan.md +143 -0
- data/templates/planning/agile/ingest_feedback.md +174 -0
- data/templates/planning/assemble_project_plan.md +113 -0
- data/templates/planning/assign_personas.md +108 -0
- data/templates/planning/create_tasks.md +52 -6
- data/templates/planning/generate_gantt.md +86 -0
- data/templates/planning/generate_wbs.md +85 -0
- data/templates/planning/initialize_planning_mode.md +70 -0
- data/templates/skills/README.md +2 -2
- data/templates/skills/marketing_strategist/SKILL.md +279 -0
- data/templates/skills/product_manager/SKILL.md +177 -0
- data/templates/skills/ruby_aidp_planning/SKILL.md +497 -0
- data/templates/skills/ruby_rspec_tdd/SKILL.md +514 -0
- data/templates/skills/ux_researcher/SKILL.md +222 -0
- metadata +47 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
require_relative "../util"
|
|
6
|
+
require_relative "../debug_mixin"
|
|
7
|
+
|
|
8
|
+
module Aidp
|
|
9
|
+
module Providers
|
|
10
|
+
class Aider < Base
|
|
11
|
+
include Aidp::DebugMixin
|
|
12
|
+
|
|
13
|
+
# Model name pattern for Aider (supports various models via OpenRouter)
|
|
14
|
+
# Aider can use any model, but we'll match common patterns
|
|
15
|
+
MODEL_PATTERN = /^(gpt-|claude-|gemini-|deepseek-|qwen-|o1-)/i
|
|
16
|
+
LONG_PROMPT_THRESHOLD = 8000
|
|
17
|
+
LONG_PROMPT_TIMEOUT = 900 # 15 minutes for large prompts
|
|
18
|
+
|
|
19
|
+
def self.available?
|
|
20
|
+
!!Aidp::Util.which("aider")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Check if this provider supports a given model family
|
|
24
|
+
#
|
|
25
|
+
# @param family_name [String] The model family name
|
|
26
|
+
# @return [Boolean] True if it matches common model patterns
|
|
27
|
+
def self.supports_model_family?(family_name)
|
|
28
|
+
MODEL_PATTERN.match?(family_name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Discover available models from registry
|
|
32
|
+
#
|
|
33
|
+
# Note: Aider uses its own configuration for models
|
|
34
|
+
# Returns registry-based models that match common patterns
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<Hash>] Array of discovered models
|
|
37
|
+
def self.discover_models
|
|
38
|
+
return [] unless available?
|
|
39
|
+
|
|
40
|
+
discover_models_from_registry(MODEL_PATTERN, "aider")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get firewall requirements for Aider provider
|
|
44
|
+
# Aider uses aider.chat for updates, openrouter.ai for API access,
|
|
45
|
+
# and pypi.org for version checking
|
|
46
|
+
def self.firewall_requirements
|
|
47
|
+
{
|
|
48
|
+
domains: [
|
|
49
|
+
"aider.chat",
|
|
50
|
+
"openrouter.ai",
|
|
51
|
+
"api.openrouter.ai",
|
|
52
|
+
"pypi.org"
|
|
53
|
+
],
|
|
54
|
+
ip_ranges: []
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def name
|
|
59
|
+
"aider"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def display_name
|
|
63
|
+
"Aider"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def available?
|
|
67
|
+
return false unless self.class.available?
|
|
68
|
+
|
|
69
|
+
# Additional check to ensure the CLI is properly configured
|
|
70
|
+
begin
|
|
71
|
+
result = Aidp::Util.execute_command("aider", ["--version"], timeout: 10)
|
|
72
|
+
result.exit_status == 0
|
|
73
|
+
rescue
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def send_message(prompt:, session: nil)
|
|
79
|
+
raise "aider CLI not available" unless self.class.available?
|
|
80
|
+
|
|
81
|
+
# Smart timeout calculation (store prompt length for adaptive logic)
|
|
82
|
+
@current_aider_prompt_length = prompt.length
|
|
83
|
+
timeout_seconds = calculate_timeout
|
|
84
|
+
|
|
85
|
+
debug_provider("aider", "Starting execution", {timeout: timeout_seconds})
|
|
86
|
+
debug_log("📝 Sending prompt to aider (length: #{prompt.length})", level: :info)
|
|
87
|
+
|
|
88
|
+
# Set up activity monitoring
|
|
89
|
+
setup_activity_monitoring("aider", method(:activity_callback))
|
|
90
|
+
record_activity("Starting aider execution")
|
|
91
|
+
|
|
92
|
+
# Create a spinner for activity display
|
|
93
|
+
spinner = TTY::Spinner.new("[:spinner] :title", format: :dots, hide_cursor: true)
|
|
94
|
+
spinner.auto_spin
|
|
95
|
+
|
|
96
|
+
activity_display_thread = Thread.new do
|
|
97
|
+
start_time = Time.now
|
|
98
|
+
loop do
|
|
99
|
+
sleep 0.5 # Update every 500ms to reduce spam
|
|
100
|
+
elapsed = Time.now - start_time
|
|
101
|
+
|
|
102
|
+
# Break if we've been running too long or state changed
|
|
103
|
+
break if elapsed > timeout_seconds || @activity_state == :completed || @activity_state == :failed
|
|
104
|
+
|
|
105
|
+
update_spinner_status(spinner, elapsed, "🤖 Aider")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
begin
|
|
110
|
+
# Use non-interactive mode with --yes-always flag and --message
|
|
111
|
+
# --yes-always is equivalent to Claude's --dangerously-skip-permissions
|
|
112
|
+
args = ["--yes-always", "--message", prompt]
|
|
113
|
+
|
|
114
|
+
# Disable aider's auto-commits by default - let AIDP handle commits
|
|
115
|
+
# based on work_loop.version_control.behavior configuration
|
|
116
|
+
args += ["--no-auto-commits"]
|
|
117
|
+
|
|
118
|
+
# Add model if configured
|
|
119
|
+
if @model && !@model.empty?
|
|
120
|
+
args += ["--model", @model]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Add session support if provided (aider supports chat history)
|
|
124
|
+
if session && !session.empty?
|
|
125
|
+
args += ["--restore-chat-history"]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# In devcontainer, aider should run in non-interactive mode
|
|
129
|
+
if in_devcontainer_or_codespace?
|
|
130
|
+
debug_log("🔓 Running aider in non-interactive mode with --yes-always (devcontainer)", level: :info)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Use debug_execute_command for better debugging
|
|
134
|
+
result = debug_execute_command("aider", args: args, timeout: timeout_seconds)
|
|
135
|
+
|
|
136
|
+
# Log the results
|
|
137
|
+
debug_command("aider", args: args, input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
|
|
138
|
+
|
|
139
|
+
if result.exit_status == 0
|
|
140
|
+
spinner.success("✓")
|
|
141
|
+
mark_completed
|
|
142
|
+
result.out
|
|
143
|
+
else
|
|
144
|
+
spinner.error("✗")
|
|
145
|
+
mark_failed("aider failed with exit code #{result.exit_status}")
|
|
146
|
+
debug_error(StandardError.new("aider failed"), {exit_code: result.exit_status, stderr: result.err})
|
|
147
|
+
raise "aider failed with exit code #{result.exit_status}: #{result.err}"
|
|
148
|
+
end
|
|
149
|
+
rescue => e
|
|
150
|
+
spinner&.error("✗")
|
|
151
|
+
mark_failed("aider execution failed: #{e.message}")
|
|
152
|
+
debug_error(e, {provider: "aider", prompt_length: prompt.length})
|
|
153
|
+
raise
|
|
154
|
+
ensure
|
|
155
|
+
cleanup_activity_display(activity_display_thread, spinner)
|
|
156
|
+
@current_aider_prompt_length = nil
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Enhanced send method with additional options
|
|
161
|
+
def send_with_options(prompt:, session: nil, model: nil, auto_commits: false)
|
|
162
|
+
args = ["--yes-always", "--message", prompt]
|
|
163
|
+
|
|
164
|
+
# Disable auto-commits by default (let AIDP handle commits)
|
|
165
|
+
# unless explicitly enabled via auto_commits parameter
|
|
166
|
+
args += if auto_commits
|
|
167
|
+
["--auto-commits"]
|
|
168
|
+
else
|
|
169
|
+
["--no-auto-commits"]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Add session support
|
|
173
|
+
if session && !session.empty?
|
|
174
|
+
args += ["--restore-chat-history"]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Add model selection (from parameter or configured model)
|
|
178
|
+
model_to_use = model || @model
|
|
179
|
+
if model_to_use
|
|
180
|
+
args += ["--model", model_to_use]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Use the enhanced version of send
|
|
184
|
+
send_with_custom_args(prompt: prompt, args: args)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Override health check for Aider specific considerations
|
|
188
|
+
def harness_healthy?
|
|
189
|
+
return false unless super
|
|
190
|
+
|
|
191
|
+
# Additional health checks specific to Aider
|
|
192
|
+
# Check if we can access the CLI (basic connectivity test)
|
|
193
|
+
begin
|
|
194
|
+
result = Aidp::Util.execute_command("aider", ["--help"], timeout: 5)
|
|
195
|
+
result.exit_status == 0
|
|
196
|
+
rescue
|
|
197
|
+
false
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
# Internal helper for send_with_options - executes with custom arguments
|
|
204
|
+
def send_with_custom_args(prompt:, args:)
|
|
205
|
+
@current_aider_prompt_length = prompt.length
|
|
206
|
+
timeout_seconds = calculate_timeout
|
|
207
|
+
|
|
208
|
+
debug_provider("aider", "Starting execution", {timeout: timeout_seconds, args: args})
|
|
209
|
+
debug_log("📝 Sending prompt to aider with custom args", level: :info)
|
|
210
|
+
|
|
211
|
+
setup_activity_monitoring("aider", method(:activity_callback))
|
|
212
|
+
record_activity("Starting aider execution with custom args")
|
|
213
|
+
|
|
214
|
+
begin
|
|
215
|
+
result = debug_execute_command("aider", args: args, timeout: timeout_seconds)
|
|
216
|
+
debug_command("aider", args: args, output: result.out, error: result.err, exit_code: result.exit_status)
|
|
217
|
+
|
|
218
|
+
if result.exit_status == 0
|
|
219
|
+
mark_completed
|
|
220
|
+
result.out
|
|
221
|
+
else
|
|
222
|
+
mark_failed("aider failed with exit code #{result.exit_status}")
|
|
223
|
+
debug_error(StandardError.new("aider failed"), {exit_code: result.exit_status, stderr: result.err})
|
|
224
|
+
raise "aider failed with exit code #{result.exit_status}: #{result.err}"
|
|
225
|
+
end
|
|
226
|
+
rescue => e
|
|
227
|
+
mark_failed("aider execution failed: #{e.message}")
|
|
228
|
+
debug_error(e, {provider: "aider", prompt_length: prompt.length})
|
|
229
|
+
raise
|
|
230
|
+
ensure
|
|
231
|
+
@current_aider_prompt_length = nil
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def activity_callback(state, message, provider)
|
|
236
|
+
# Handle activity state changes
|
|
237
|
+
case state
|
|
238
|
+
when :stuck
|
|
239
|
+
display_message("\n⚠️ Aider appears stuck: #{message}", type: :warning)
|
|
240
|
+
when :completed
|
|
241
|
+
display_message("\n✅ Aider completed: #{message}", type: :success)
|
|
242
|
+
when :failed
|
|
243
|
+
display_message("\n❌ Aider failed: #{message}", type: :error)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def calculate_timeout
|
|
248
|
+
env_override = ENV["AIDP_AIDER_TIMEOUT"]
|
|
249
|
+
return env_override.to_i if env_override&.match?(/^\d+$/)
|
|
250
|
+
|
|
251
|
+
base_timeout = super
|
|
252
|
+
|
|
253
|
+
prompt_length = @current_aider_prompt_length
|
|
254
|
+
return base_timeout unless prompt_length && prompt_length >= LONG_PROMPT_THRESHOLD
|
|
255
|
+
|
|
256
|
+
extended_timeout = [base_timeout, LONG_PROMPT_TIMEOUT].max
|
|
257
|
+
if extended_timeout > base_timeout
|
|
258
|
+
display_message("⏱️ Aider prompt length #{prompt_length} detected - extending timeout to #{extended_timeout} seconds", type: :info)
|
|
259
|
+
end
|
|
260
|
+
extended_timeout
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -9,10 +9,155 @@ module Aidp
|
|
|
9
9
|
class Anthropic < Base
|
|
10
10
|
include Aidp::DebugMixin
|
|
11
11
|
|
|
12
|
+
# Model name pattern for Anthropic Claude models
|
|
13
|
+
MODEL_PATTERN = /^claude-[\d.-]+-(?:opus|sonnet|haiku)(?:-\d{8})?$/i
|
|
14
|
+
|
|
12
15
|
def self.available?
|
|
13
16
|
!!Aidp::Util.which("claude")
|
|
14
17
|
end
|
|
15
18
|
|
|
19
|
+
# Normalize a provider-specific model name to its model family
|
|
20
|
+
#
|
|
21
|
+
# Anthropic uses date-versioned models (e.g., "claude-3-5-sonnet-20241022").
|
|
22
|
+
# This method strips the date suffix to get the family name.
|
|
23
|
+
#
|
|
24
|
+
# @param provider_model_name [String] The versioned model name
|
|
25
|
+
# @return [String] The model family name (e.g., "claude-3-5-sonnet")
|
|
26
|
+
def self.model_family(provider_model_name)
|
|
27
|
+
# Strip date suffix: "claude-3-5-sonnet-20241022" → "claude-3-5-sonnet"
|
|
28
|
+
provider_model_name.sub(/-\d{8}$/, "")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Convert a model family name to the provider's preferred model name
|
|
32
|
+
#
|
|
33
|
+
# Returns the family name as-is. Users can configure specific versions in aidp.yml.
|
|
34
|
+
#
|
|
35
|
+
# @param family_name [String] The model family name
|
|
36
|
+
# @return [String] The model name (same as family for flexibility)
|
|
37
|
+
def self.provider_model_name(family_name)
|
|
38
|
+
family_name
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if this provider supports a given model family
|
|
42
|
+
#
|
|
43
|
+
# @param family_name [String] The model family name
|
|
44
|
+
# @return [Boolean] True if it matches Claude model pattern
|
|
45
|
+
def self.supports_model_family?(family_name)
|
|
46
|
+
MODEL_PATTERN.match?(family_name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Discover available models from Claude CLI
|
|
50
|
+
#
|
|
51
|
+
# @return [Array<Hash>] Array of discovered models
|
|
52
|
+
def self.discover_models
|
|
53
|
+
return [] unless available?
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
require "open3"
|
|
57
|
+
output, _, status = Open3.capture3("claude", "models", "list", {timeout: 10})
|
|
58
|
+
return [] unless status.success?
|
|
59
|
+
|
|
60
|
+
parse_models_list(output)
|
|
61
|
+
rescue => e
|
|
62
|
+
Aidp.log_debug("anthropic_provider", "discovery failed", error: e.message)
|
|
63
|
+
[]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get firewall requirements for Anthropic provider
|
|
68
|
+
def self.firewall_requirements
|
|
69
|
+
{
|
|
70
|
+
domains: [
|
|
71
|
+
"api.anthropic.com",
|
|
72
|
+
"claude.ai",
|
|
73
|
+
"console.anthropic.com"
|
|
74
|
+
],
|
|
75
|
+
ip_ranges: []
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class << self
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def parse_models_list(output)
|
|
83
|
+
return [] if output.nil? || output.empty?
|
|
84
|
+
|
|
85
|
+
models = []
|
|
86
|
+
lines = output.lines.map(&:strip)
|
|
87
|
+
|
|
88
|
+
# Skip header and separator lines
|
|
89
|
+
lines.reject! { |line| line.empty? || line.match?(/^[-=]+$/) || line.match?(/^(Model|Name)/i) }
|
|
90
|
+
|
|
91
|
+
lines.each do |line|
|
|
92
|
+
model_info = parse_model_line(line)
|
|
93
|
+
models << model_info if model_info
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
Aidp.log_info("anthropic_provider", "discovered models", count: models.size)
|
|
97
|
+
models
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_model_line(line)
|
|
101
|
+
# Format 1: Simple list of model names
|
|
102
|
+
if line.match?(/^claude-\d/)
|
|
103
|
+
model_name = line.split.first
|
|
104
|
+
return build_model_info(model_name)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Format 2: Table format with columns
|
|
108
|
+
parts = line.split(/\s{2,}/)
|
|
109
|
+
if parts.size >= 1 && parts[0].match?(/^claude/)
|
|
110
|
+
model_name = parts[0]
|
|
111
|
+
model_name = "#{model_name}-#{parts[1]}" if parts.size > 1 && parts[1].match?(/^\d{8}$/)
|
|
112
|
+
return build_model_info(model_name)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Format 3: JSON-like or key-value pairs
|
|
116
|
+
if line.match?(/name:\s*(.+)/)
|
|
117
|
+
model_name = $1.strip
|
|
118
|
+
return build_model_info(model_name)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def build_model_info(model_name)
|
|
125
|
+
family = model_family(model_name)
|
|
126
|
+
tier = classify_tier(model_name)
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
name: model_name,
|
|
130
|
+
family: family,
|
|
131
|
+
tier: tier,
|
|
132
|
+
capabilities: extract_capabilities(model_name),
|
|
133
|
+
context_window: infer_context_window(family),
|
|
134
|
+
provider: "anthropic"
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def classify_tier(model_name)
|
|
139
|
+
name_lower = model_name.downcase
|
|
140
|
+
return "advanced" if name_lower.include?("opus")
|
|
141
|
+
return "mini" if name_lower.include?("haiku")
|
|
142
|
+
return "standard" if name_lower.include?("sonnet")
|
|
143
|
+
"standard"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def extract_capabilities(model_name)
|
|
147
|
+
capabilities = ["chat", "code"]
|
|
148
|
+
name_lower = model_name.downcase
|
|
149
|
+
capabilities << "vision" unless name_lower.include?("haiku")
|
|
150
|
+
capabilities
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def infer_context_window(family)
|
|
154
|
+
family.match?(/claude-3/) ? 200_000 : nil
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Public instance methods (called from workflows and harness)
|
|
159
|
+
public
|
|
160
|
+
|
|
16
161
|
def name
|
|
17
162
|
"anthropic"
|
|
18
163
|
end
|
|
@@ -53,8 +198,7 @@ module Aidp
|
|
|
53
198
|
supports_json_mode: true,
|
|
54
199
|
supports_tool_use: true,
|
|
55
200
|
supports_vision: false,
|
|
56
|
-
supports_file_upload: true
|
|
57
|
-
streaming: true
|
|
201
|
+
supports_file_upload: true
|
|
58
202
|
}
|
|
59
203
|
end
|
|
60
204
|
|
|
@@ -115,18 +259,12 @@ module Aidp
|
|
|
115
259
|
debug_provider("claude", "Starting execution", {timeout: timeout_seconds})
|
|
116
260
|
debug_log("📝 Sending prompt to claude...", level: :info)
|
|
117
261
|
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
# Claude CLI requires --verbose when using --print with --output-format=stream-json
|
|
125
|
-
args += ["--verbose", "--output-format=stream-json", "--include-partial-messages"]
|
|
126
|
-
display_message("📺 True streaming enabled - real-time chunks from Claude API", type: :info)
|
|
127
|
-
else
|
|
128
|
-
# Use text format for non-streaming (default behavior)
|
|
129
|
-
args += ["--output-format=text"]
|
|
262
|
+
# Build command arguments
|
|
263
|
+
args = ["--print", "--output-format=text"]
|
|
264
|
+
|
|
265
|
+
# Add model if specified
|
|
266
|
+
if @model && !@model.empty?
|
|
267
|
+
args << "--model" << @model
|
|
130
268
|
end
|
|
131
269
|
|
|
132
270
|
# Check if we should skip permissions (devcontainer support)
|
|
@@ -136,28 +274,34 @@ module Aidp
|
|
|
136
274
|
end
|
|
137
275
|
|
|
138
276
|
begin
|
|
139
|
-
|
|
140
|
-
result = debug_execute_command("claude", args: args, input: prompt, timeout: timeout_seconds, streaming: streaming_enabled)
|
|
277
|
+
result = debug_execute_command("claude", args: args, input: prompt, timeout: timeout_seconds)
|
|
141
278
|
|
|
142
279
|
# Log the results
|
|
143
280
|
debug_command("claude", args: args, input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
|
|
144
281
|
|
|
145
282
|
if result.exit_status == 0
|
|
146
|
-
|
|
147
|
-
if streaming_enabled && args.include?("--output-format=stream-json")
|
|
148
|
-
# Parse stream-json output and extract final content
|
|
149
|
-
parse_stream_json_output(result.out)
|
|
150
|
-
else
|
|
151
|
-
# Return text output as-is
|
|
152
|
-
result.out
|
|
153
|
-
end
|
|
283
|
+
result.out
|
|
154
284
|
else
|
|
155
|
-
# Detect
|
|
285
|
+
# Detect issues in stdout/stderr (Claude sometimes prints to stdout)
|
|
156
286
|
combined = [result.out, result.err].compact.join("\n")
|
|
287
|
+
|
|
288
|
+
# Check for rate limit (Session limit reached)
|
|
289
|
+
if combined.match?(/session limit reached/i)
|
|
290
|
+
Aidp.log_debug("anthropic_provider", "rate_limit_detected",
|
|
291
|
+
exit_code: result.exit_status,
|
|
292
|
+
message: combined)
|
|
293
|
+
notify_rate_limit(combined)
|
|
294
|
+
error_message = "Rate limit reached for Claude CLI.\n#{combined}"
|
|
295
|
+
debug_error(StandardError.new(error_message), {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
296
|
+
raise error_message
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Check for auth issues
|
|
157
300
|
if combined.downcase.include?("oauth token has expired") || combined.downcase.include?("authentication_error")
|
|
158
|
-
error_message = "Authentication error from Claude CLI: token expired or invalid
|
|
301
|
+
error_message = "Authentication error from Claude CLI: token expired or invalid.\n" \
|
|
302
|
+
"Run 'claude /login' or refresh credentials.\n" \
|
|
303
|
+
"Note: Model discovery requires valid authentication."
|
|
159
304
|
debug_error(StandardError.new(error_message), {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
160
|
-
# Raise a recognizable error for classifier
|
|
161
305
|
raise error_message
|
|
162
306
|
end
|
|
163
307
|
|
|
@@ -172,58 +316,47 @@ module Aidp
|
|
|
172
316
|
|
|
173
317
|
private
|
|
174
318
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# 2. Environment variable override
|
|
179
|
-
# 3. Adaptive timeout based on step type
|
|
180
|
-
# 4. Default timeout
|
|
319
|
+
# Notify harness about rate limit detection
|
|
320
|
+
def notify_rate_limit(message)
|
|
321
|
+
return unless @harness_context
|
|
181
322
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return TIMEOUT_QUICK_MODE
|
|
185
|
-
end
|
|
323
|
+
# Extract reset time from message (e.g., "resets 4am")
|
|
324
|
+
reset_time = extract_reset_time_from_message(message)
|
|
186
325
|
|
|
187
|
-
if
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
326
|
+
# Notify provider manager if available
|
|
327
|
+
if @harness_context.respond_to?(:provider_manager)
|
|
328
|
+
provider_manager = @harness_context.provider_manager
|
|
329
|
+
if provider_manager.respond_to?(:mark_rate_limited)
|
|
330
|
+
provider_manager.mark_rate_limited("anthropic", reset_time)
|
|
331
|
+
Aidp.log_debug("anthropic_provider", "notified_provider_manager",
|
|
332
|
+
reset_time: reset_time)
|
|
333
|
+
end
|
|
194
334
|
end
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
TIMEOUT_DEFAULT
|
|
335
|
+
rescue => e
|
|
336
|
+
Aidp.log_debug("anthropic_provider", "notify_rate_limit_failed",
|
|
337
|
+
error: e.message)
|
|
199
338
|
end
|
|
200
339
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
when /STATIC_ANALYSIS/
|
|
218
|
-
TIMEOUT_STATIC_ANALYSIS
|
|
219
|
-
when /REFACTORING_RECOMMENDATIONS/
|
|
220
|
-
TIMEOUT_REFACTORING_RECOMMENDATIONS
|
|
221
|
-
when /IMPLEMENTATION/
|
|
222
|
-
TIMEOUT_IMPLEMENTATION
|
|
223
|
-
else
|
|
224
|
-
nil # Use default
|
|
225
|
-
end
|
|
340
|
+
# Extract reset time from rate limit message
|
|
341
|
+
def extract_reset_time_from_message(message)
|
|
342
|
+
# Handle expressions like "resets 4am" or "reset at 4:30pm"
|
|
343
|
+
time_of_day_match = message.match(/reset(?:s)?(?:\s+at)?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i)
|
|
344
|
+
if time_of_day_match
|
|
345
|
+
hour = time_of_day_match[1].to_i
|
|
346
|
+
minute = time_of_day_match[2] ? time_of_day_match[2].to_i : 0
|
|
347
|
+
meridiem = time_of_day_match[3].downcase
|
|
348
|
+
|
|
349
|
+
hour %= 12
|
|
350
|
+
hour += 12 if meridiem == "pm"
|
|
351
|
+
|
|
352
|
+
now = Time.now
|
|
353
|
+
reset_time = Time.new(now.year, now.month, now.day, hour, minute, 0, now.utc_offset)
|
|
354
|
+
reset_time += 86_400 if reset_time <= now
|
|
355
|
+
return reset_time
|
|
226
356
|
end
|
|
357
|
+
|
|
358
|
+
# Default to 1 hour from now if no specific time found
|
|
359
|
+
Time.now + 3600
|
|
227
360
|
end
|
|
228
361
|
|
|
229
362
|
# Check if we should skip permissions based on devcontainer configuration
|
|
@@ -243,54 +376,6 @@ module Aidp
|
|
|
243
376
|
false
|
|
244
377
|
end
|
|
245
378
|
|
|
246
|
-
# Parse stream-json output from Claude CLI
|
|
247
|
-
def parse_stream_json_output(output)
|
|
248
|
-
return output if output.nil? || output.empty?
|
|
249
|
-
|
|
250
|
-
# Stream-json output contains multiple JSON objects, one per line
|
|
251
|
-
# We want to extract the final content from the last complete message
|
|
252
|
-
lines = output.strip.split("\n")
|
|
253
|
-
content_parts = []
|
|
254
|
-
|
|
255
|
-
lines.each do |line|
|
|
256
|
-
next if line.strip.empty?
|
|
257
|
-
|
|
258
|
-
begin
|
|
259
|
-
json_obj = JSON.parse(line)
|
|
260
|
-
|
|
261
|
-
# Look for content in various possible structures
|
|
262
|
-
if json_obj["type"] == "content_block_delta" && json_obj["delta"] && json_obj["delta"]["text"]
|
|
263
|
-
content_parts << json_obj["delta"]["text"]
|
|
264
|
-
elsif json_obj["content"]&.is_a?(Array)
|
|
265
|
-
json_obj["content"].each do |content_item|
|
|
266
|
-
content_parts << content_item["text"] if content_item["text"]
|
|
267
|
-
end
|
|
268
|
-
elsif json_obj["message"] && json_obj["message"]["content"]
|
|
269
|
-
if json_obj["message"]["content"].is_a?(Array)
|
|
270
|
-
json_obj["message"]["content"].each do |content_item|
|
|
271
|
-
content_parts << content_item["text"] if content_item["text"]
|
|
272
|
-
end
|
|
273
|
-
elsif json_obj["message"]["content"].is_a?(String)
|
|
274
|
-
content_parts << json_obj["message"]["content"]
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
rescue JSON::ParserError => e
|
|
278
|
-
debug_log("⚠️ Failed to parse JSON line: #{e.message}", level: :warn, data: {line: line})
|
|
279
|
-
# If JSON parsing fails, treat as plain text
|
|
280
|
-
content_parts << line
|
|
281
|
-
end
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
result = content_parts.join
|
|
285
|
-
|
|
286
|
-
# Fallback: if no content found in JSON, return original output
|
|
287
|
-
result.empty? ? output : result
|
|
288
|
-
rescue => e
|
|
289
|
-
debug_log("⚠️ Failed to parse stream-json output: #{e.message}", level: :warn)
|
|
290
|
-
# Return original output if parsing fails
|
|
291
|
-
output
|
|
292
|
-
end
|
|
293
|
-
|
|
294
379
|
# Parse Claude MCP server list output
|
|
295
380
|
def parse_claude_mcp_output(output)
|
|
296
381
|
servers = []
|