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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +89 -0
  3. data/lib/aidp/cli/checkpoint_command.rb +198 -0
  4. data/lib/aidp/cli/config_command.rb +71 -0
  5. data/lib/aidp/cli/enhanced_input.rb +2 -0
  6. data/lib/aidp/cli/first_run_wizard.rb +8 -7
  7. data/lib/aidp/cli/harness_command.rb +102 -0
  8. data/lib/aidp/cli/jobs_command.rb +3 -3
  9. data/lib/aidp/cli/mcp_dashboard.rb +4 -3
  10. data/lib/aidp/cli/models_command.rb +661 -0
  11. data/lib/aidp/cli/providers_command.rb +223 -0
  12. data/lib/aidp/cli.rb +45 -464
  13. data/lib/aidp/config.rb +54 -0
  14. data/lib/aidp/daemon/runner.rb +2 -2
  15. data/lib/aidp/debug_mixin.rb +25 -10
  16. data/lib/aidp/execute/agent_signal_parser.rb +22 -0
  17. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  18. data/lib/aidp/execute/checkpoint_display.rb +38 -37
  19. data/lib/aidp/execute/interactive_repl.rb +2 -1
  20. data/lib/aidp/execute/prompt_manager.rb +4 -4
  21. data/lib/aidp/execute/repl_macros.rb +2 -2
  22. data/lib/aidp/execute/steps.rb +94 -1
  23. data/lib/aidp/execute/work_loop_runner.rb +238 -19
  24. data/lib/aidp/execute/workflow_selector.rb +4 -27
  25. data/lib/aidp/firewall/provider_requirements_collector.rb +262 -0
  26. data/lib/aidp/harness/ai_decision_engine.rb +35 -2
  27. data/lib/aidp/harness/config_manager.rb +5 -10
  28. data/lib/aidp/harness/config_schema.rb +8 -0
  29. data/lib/aidp/harness/configuration.rb +40 -2
  30. data/lib/aidp/harness/enhanced_runner.rb +25 -19
  31. data/lib/aidp/harness/error_handler.rb +23 -73
  32. data/lib/aidp/harness/model_cache.rb +269 -0
  33. data/lib/aidp/harness/model_discovery_service.rb +259 -0
  34. data/lib/aidp/harness/model_registry.rb +201 -0
  35. data/lib/aidp/harness/provider_factory.rb +11 -2
  36. data/lib/aidp/harness/runner.rb +5 -0
  37. data/lib/aidp/harness/state_manager.rb +0 -7
  38. data/lib/aidp/harness/thinking_depth_manager.rb +202 -7
  39. data/lib/aidp/harness/ui/enhanced_tui.rb +8 -18
  40. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +0 -18
  41. data/lib/aidp/harness/ui/progress_display.rb +6 -2
  42. data/lib/aidp/harness/user_interface.rb +0 -58
  43. data/lib/aidp/init/runner.rb +7 -2
  44. data/lib/aidp/message_display.rb +0 -46
  45. data/lib/aidp/planning/analyzers/feedback_analyzer.rb +365 -0
  46. data/lib/aidp/planning/builders/agile_plan_builder.rb +387 -0
  47. data/lib/aidp/planning/builders/project_plan_builder.rb +193 -0
  48. data/lib/aidp/planning/generators/gantt_generator.rb +190 -0
  49. data/lib/aidp/planning/generators/iteration_plan_generator.rb +392 -0
  50. data/lib/aidp/planning/generators/legacy_research_planner.rb +473 -0
  51. data/lib/aidp/planning/generators/marketing_report_generator.rb +348 -0
  52. data/lib/aidp/planning/generators/mvp_scope_generator.rb +310 -0
  53. data/lib/aidp/planning/generators/user_test_plan_generator.rb +373 -0
  54. data/lib/aidp/planning/generators/wbs_generator.rb +259 -0
  55. data/lib/aidp/planning/mappers/persona_mapper.rb +163 -0
  56. data/lib/aidp/planning/parsers/document_parser.rb +141 -0
  57. data/lib/aidp/planning/parsers/feedback_data_parser.rb +252 -0
  58. data/lib/aidp/provider_manager.rb +8 -32
  59. data/lib/aidp/providers/adapter.rb +2 -4
  60. data/lib/aidp/providers/aider.rb +264 -0
  61. data/lib/aidp/providers/anthropic.rb +206 -121
  62. data/lib/aidp/providers/base.rb +123 -3
  63. data/lib/aidp/providers/capability_registry.rb +0 -1
  64. data/lib/aidp/providers/codex.rb +75 -70
  65. data/lib/aidp/providers/cursor.rb +87 -59
  66. data/lib/aidp/providers/gemini.rb +57 -60
  67. data/lib/aidp/providers/github_copilot.rb +19 -66
  68. data/lib/aidp/providers/kilocode.rb +35 -80
  69. data/lib/aidp/providers/opencode.rb +35 -80
  70. data/lib/aidp/setup/wizard.rb +555 -8
  71. data/lib/aidp/version.rb +1 -1
  72. data/lib/aidp/watch/build_processor.rb +211 -30
  73. data/lib/aidp/watch/change_request_processor.rb +128 -14
  74. data/lib/aidp/watch/ci_fix_processor.rb +103 -37
  75. data/lib/aidp/watch/ci_log_extractor.rb +258 -0
  76. data/lib/aidp/watch/github_state_extractor.rb +177 -0
  77. data/lib/aidp/watch/implementation_verifier.rb +284 -0
  78. data/lib/aidp/watch/plan_generator.rb +95 -52
  79. data/lib/aidp/watch/plan_processor.rb +7 -6
  80. data/lib/aidp/watch/repository_client.rb +245 -17
  81. data/lib/aidp/watch/review_processor.rb +100 -19
  82. data/lib/aidp/watch/reviewers/base_reviewer.rb +1 -1
  83. data/lib/aidp/watch/runner.rb +181 -29
  84. data/lib/aidp/watch/state_store.rb +22 -1
  85. data/lib/aidp/workflows/definitions.rb +147 -0
  86. data/lib/aidp/workflows/guided_agent.rb +3 -3
  87. data/lib/aidp/workstream_cleanup.rb +245 -0
  88. data/lib/aidp/worktree.rb +19 -0
  89. data/templates/aidp-development.yml.example +2 -2
  90. data/templates/aidp-production.yml.example +3 -3
  91. data/templates/aidp.yml.example +57 -0
  92. data/templates/implementation/generate_tdd_specs.md +213 -0
  93. data/templates/implementation/iterative_implementation.md +122 -0
  94. data/templates/planning/agile/analyze_feedback.md +183 -0
  95. data/templates/planning/agile/generate_iteration_plan.md +179 -0
  96. data/templates/planning/agile/generate_legacy_research_plan.md +171 -0
  97. data/templates/planning/agile/generate_marketing_report.md +162 -0
  98. data/templates/planning/agile/generate_mvp_scope.md +127 -0
  99. data/templates/planning/agile/generate_user_test_plan.md +143 -0
  100. data/templates/planning/agile/ingest_feedback.md +174 -0
  101. data/templates/planning/assemble_project_plan.md +113 -0
  102. data/templates/planning/assign_personas.md +108 -0
  103. data/templates/planning/create_tasks.md +52 -6
  104. data/templates/planning/generate_gantt.md +86 -0
  105. data/templates/planning/generate_wbs.md +85 -0
  106. data/templates/planning/initialize_planning_mode.md +70 -0
  107. data/templates/skills/README.md +2 -2
  108. data/templates/skills/marketing_strategist/SKILL.md +279 -0
  109. data/templates/skills/product_manager/SKILL.md +177 -0
  110. data/templates/skills/ruby_aidp_planning/SKILL.md +497 -0
  111. data/templates/skills/ruby_rspec_tdd/SKILL.md +514 -0
  112. data/templates/skills/ux_researcher/SKILL.md +222 -0
  113. 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
- # Check if streaming mode is enabled
119
- streaming_enabled = ENV["AIDP_STREAMING"] == "1" || ENV["DEBUG"] == "1"
120
-
121
- # Build command arguments with proper streaming support
122
- args = ["--print"]
123
- if streaming_enabled
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
- # Use debug_execute_command with streaming support
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
- # Handle different output formats
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 auth issues in stdout/stderr (Claude sometimes prints JSON with auth error to stdout)
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. Run 'claude /login' or refresh credentials."
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
- def calculate_timeout
176
- # Priority order for timeout calculation:
177
- # 1. Quick mode (for testing)
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
- if ENV["AIDP_QUICK_MODE"]
183
- display_message("⚡ Quick mode enabled - #{TIMEOUT_QUICK_MODE / 60} minute timeout", type: :highlight)
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 ENV["AIDP_ANTHROPIC_TIMEOUT"]
188
- return ENV["AIDP_ANTHROPIC_TIMEOUT"].to_i
189
- end
190
-
191
- if adaptive_timeout
192
- display_message("🧠 Using adaptive timeout: #{adaptive_timeout} seconds", type: :info)
193
- return adaptive_timeout
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
- # Default timeout
197
- display_message("📋 Using default timeout: #{TIMEOUT_DEFAULT / 60} minutes", type: :info)
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
- def adaptive_timeout
202
- @adaptive_timeout ||= begin
203
- # Timeout recommendations based on step type patterns
204
- step_name = ENV["AIDP_CURRENT_STEP"] || ""
205
-
206
- case step_name
207
- when /REPOSITORY_ANALYSIS/
208
- TIMEOUT_REPOSITORY_ANALYSIS
209
- when /ARCHITECTURE_ANALYSIS/
210
- TIMEOUT_ARCHITECTURE_ANALYSIS
211
- when /TEST_ANALYSIS/
212
- TIMEOUT_TEST_ANALYSIS
213
- when /FUNCTIONALITY_ANALYSIS/
214
- TIMEOUT_FUNCTIONALITY_ANALYSIS
215
- when /DOCUMENTATION_ANALYSIS/
216
- TIMEOUT_DOCUMENTATION_ANALYSIS
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 = []