aidp 0.14.1 → 0.14.2

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.
@@ -2,11 +2,14 @@
2
2
 
3
3
  require "json"
4
4
  require "fileutils"
5
+ require "aidp/rescue_logging"
5
6
 
6
7
  module Aidp
7
8
  module Storage
8
9
  # Simple JSON file storage for structured data
9
10
  class JsonStorage
11
+ include Aidp::RescueLogging
12
+
10
13
  def initialize(base_dir = ".aidp")
11
14
  @base_dir = base_dir
12
15
  ensure_directory_exists
@@ -32,11 +35,13 @@ module Aidp
32
35
  success: true
33
36
  }
34
37
  rescue => error
35
- {
38
+ log_rescue(error,
39
+ component: "json_storage",
40
+ action: "store",
41
+ fallback: {success: false},
36
42
  filename: filename,
37
- error: error.message,
38
- success: false
39
- }
43
+ path: file_path)
44
+ {filename: filename, error: error.message, success: false}
40
45
  end
41
46
 
42
47
  # Load data from JSON file
@@ -48,7 +53,12 @@ module Aidp
48
53
  json_data = JSON.parse(content)
49
54
  json_data["data"]
50
55
  rescue => error
51
- puts "Error loading #{filename}: #{error.message}" if ENV["AIDP_DEBUG"]
56
+ log_rescue(error,
57
+ component: "json_storage",
58
+ action: "load",
59
+ fallback: nil,
60
+ filename: filename,
61
+ path: (defined?(file_path) ? file_path : nil))
52
62
  nil
53
63
  end
54
64
 
@@ -72,11 +82,13 @@ module Aidp
72
82
  success: true
73
83
  }
74
84
  rescue => error
75
- {
85
+ log_rescue(error,
86
+ component: "json_storage",
87
+ action: "update",
88
+ fallback: {success: false},
76
89
  filename: filename,
77
- error: error.message,
78
- success: false
79
- }
90
+ path: (defined?(file_path) ? file_path : nil))
91
+ {filename: filename, error: error.message, success: false}
80
92
  end
81
93
 
82
94
  # Check if file exists
@@ -92,6 +104,12 @@ module Aidp
92
104
  File.delete(file_path)
93
105
  {success: true, message: "File deleted"}
94
106
  rescue => error
107
+ log_rescue(error,
108
+ component: "json_storage",
109
+ action: "delete",
110
+ fallback: {success: false},
111
+ filename: filename,
112
+ path: file_path)
95
113
  {success: false, error: error.message}
96
114
  end
97
115
 
@@ -120,7 +138,12 @@ module Aidp
120
138
  size: File.size(file_path)
121
139
  }
122
140
  rescue => error
123
- puts "Error getting metadata for #{filename}: #{error.message}" if ENV["AIDP_DEBUG"]
141
+ log_rescue(error,
142
+ component: "json_storage",
143
+ action: "metadata",
144
+ fallback: nil,
145
+ filename: filename,
146
+ path: (defined?(file_path) ? file_path : nil))
124
147
  nil
125
148
  end
126
149
 
data/lib/aidp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.14.1"
4
+ VERSION = "0.14.2"
5
5
  end
@@ -5,6 +5,7 @@ require_relative "../harness/provider_factory"
5
5
  require_relative "../harness/config_manager"
6
6
  require_relative "definitions"
7
7
  require_relative "../message_display"
8
+ require_relative "../debug_mixin"
8
9
  require_relative "../cli/enhanced_input"
9
10
 
10
11
  module Aidp
@@ -13,6 +14,7 @@ module Aidp
13
14
  # Acts as a copilot to match user intent to AIDP capabilities
14
15
  class GuidedAgent
15
16
  include Aidp::MessageDisplay
17
+ include Aidp::DebugMixin
16
18
 
17
19
  class ConversationError < StandardError; end
18
20
 
@@ -39,6 +41,8 @@ module Aidp
39
41
  display_message("\n🤖 Welcome to AIDP Guided Workflow!", type: :highlight)
40
42
  display_message("I'll help you plan and execute your project.\n", type: :info)
41
43
 
44
+ validate_provider_configuration!
45
+
42
46
  plan_and_execute_workflow
43
47
  rescue => e
44
48
  raise ConversationError, "Failed to guide workflow selection: #{e.message}"
@@ -70,10 +74,18 @@ module Aidp
70
74
 
71
75
  @conversation_history << {role: "user", content: goal}
72
76
 
77
+ iteration = 0
73
78
  loop do
79
+ iteration += 1
74
80
  # Ask AI for next question based on current plan
75
81
  question_response = get_planning_questions(plan)
76
82
 
83
+ # Debug: show raw provider response and parsed result
84
+ debug_log("Planning iteration #{iteration} provider response", level: :debug, data: {
85
+ raw_response: question_response[:raw_response]&.inspect,
86
+ parsed: question_response.inspect
87
+ })
88
+
77
89
  # If AI says plan is complete, confirm with user
78
90
  if question_response[:complete]
79
91
  display_message("\n✅ Plan Summary", type: :highlight)
@@ -96,6 +108,12 @@ module Aidp
96
108
  # Update plan with answer
97
109
  update_plan_from_answer(plan, question, answer)
98
110
  end
111
+
112
+ # Guard: break loop after 10 iterations to avoid infinite loop
113
+ if iteration >= 10
114
+ display_message("[ERROR] Planning loop exceeded 10 iterations. Provider may be returning generic responses.", type: :error)
115
+ break
116
+ end
99
117
  end
100
118
 
101
119
  plan
@@ -105,8 +123,18 @@ module Aidp
105
123
  system_prompt = build_planning_system_prompt
106
124
  user_prompt = build_planning_prompt(plan)
107
125
 
126
+ # If requirements are already detailed, ask provider to check for completion
127
+ requirements = plan[:requirements]
128
+ requirements_detailed = requirements.is_a?(Hash) && requirements.values.flatten.any? { |r| r.length > 50 }
129
+ if requirements_detailed
130
+ user_prompt += "\n\nNOTE: Requirements have been provided in detail above. If you have enough information, set 'complete' to true. Do not repeat the same requirements question."
131
+ end
132
+
108
133
  response = call_provider_for_analysis(system_prompt, user_prompt)
109
- parse_planning_response(response)
134
+ parsed = parse_planning_response(response)
135
+ # Attach raw response for debug
136
+ parsed[:raw_response] = response
137
+ parsed
110
138
  end
111
139
 
112
140
  def identify_steps_from_plan(plan)
@@ -135,6 +163,16 @@ module Aidp
135
163
  end
136
164
 
137
165
  def build_workflow_from_plan(plan, needed_steps)
166
+ # Filter out any unknown steps to avoid nil dereference if SPEC changed or an AI hallucinated a step key
167
+ execute_spec = Aidp::Execute::Steps::SPEC
168
+ unknown_steps = needed_steps.reject { |s| execute_spec.key?(s) }
169
+ if unknown_steps.any?
170
+ display_message("⚠️ Ignoring unknown execute steps: #{unknown_steps.join(", ")}", type: :warning)
171
+ needed_steps -= unknown_steps
172
+ end
173
+
174
+ details = needed_steps.map { |step| execute_spec[step]["description"] }
175
+
138
176
  {
139
177
  mode: :execute,
140
178
  workflow_key: :plan_and_execute,
@@ -144,7 +182,7 @@ module Aidp
144
182
  workflow: {
145
183
  name: "Plan & Execute",
146
184
  description: "Custom workflow from iterative planning",
147
- details: needed_steps.map { |step| Aidp::Execute::Steps::SPEC[step]["description"] }
185
+ details: details
148
186
  },
149
187
  completion_criteria: plan[:completion_criteria]
150
188
  }
@@ -233,33 +271,52 @@ module Aidp
233
271
  end
234
272
 
235
273
  def call_provider_for_analysis(system_prompt, user_prompt)
236
- # Get current provider from provider manager
237
- provider_name = @provider_manager.current_provider
274
+ attempts = 0
275
+ max_attempts = (@provider_manager.respond_to?(:configured_providers) ? @provider_manager.configured_providers.size : 2)
276
+ max_attempts = 2 if max_attempts < 2 # at least one retry if a fallback exists
238
277
 
239
- unless provider_name
240
- raise ConversationError, "No provider configured for guided workflow"
241
- end
278
+ begin
279
+ attempts += 1
242
280
 
243
- # Create provider instance using ProviderFactory
244
- provider_factory = Aidp::Harness::ProviderFactory.new(@config_manager)
245
- provider = provider_factory.create_provider(provider_name, prompt: @prompt)
281
+ provider_name = @provider_manager.current_provider
282
+ unless provider_name
283
+ raise ConversationError, "No provider configured for guided workflow"
284
+ end
246
285
 
247
- unless provider
248
- raise ConversationError, "Failed to create provider instance for #{provider_name}"
249
- end
286
+ # Create provider instance using ProviderFactory
287
+ provider_factory = Aidp::Harness::ProviderFactory.new(@config_manager)
288
+ provider = provider_factory.create_provider(provider_name, prompt: @prompt)
250
289
 
251
- # Make the request - combine system and user prompts
252
- combined_prompt = "#{system_prompt}\n\n#{user_prompt}"
290
+ unless provider
291
+ raise ConversationError, "Failed to create provider instance for #{provider_name}"
292
+ end
253
293
 
254
- result = provider.send(prompt: combined_prompt)
294
+ combined_prompt = "#{system_prompt}\n\n#{user_prompt}"
295
+ result = provider.send(prompt: combined_prompt)
255
296
 
256
- # Provider.send returns a string (the content), not a hash
257
- # Check if result is nil or empty which indicates failure
258
- if result.nil? || result.empty?
259
- raise ConversationError, "Provider request failed: empty response"
260
- end
297
+ if result.nil? || result.empty?
298
+ raise ConversationError, "Provider request failed: empty response"
299
+ end
300
+
301
+ result
302
+ rescue => e
303
+ message = e.message.to_s
304
+ classified = if message =~ /resource[_ ]exhausted/i || message =~ /\[resource_exhausted\]/i
305
+ "resource_exhausted"
306
+ elsif message =~ /quota[_ ]exceeded/i || message =~ /\[quota_exceeded\]/i
307
+ "quota_exceeded"
308
+ end
261
309
 
262
- result
310
+ if classified && attempts < max_attempts
311
+ display_message("⚠️ Provider '#{provider_name}' #{classified.tr("_", " ")} – attempting fallback...", type: :warning)
312
+ switched = @provider_manager.switch_provider_for_error(classified, stderr: message) if @provider_manager.respond_to?(:switch_provider_for_error)
313
+ if switched && switched != provider_name
314
+ display_message("↩️ Switched to provider '#{switched}'", type: :info)
315
+ retry
316
+ end
317
+ end
318
+ raise
319
+ end
263
320
  end
264
321
 
265
322
  def parse_recommendation(response_text)
@@ -276,6 +333,22 @@ module Aidp
276
333
  raise ConversationError, "Invalid JSON in recommendation: #{e.message}"
277
334
  end
278
335
 
336
+ def validate_provider_configuration!
337
+ configured = @provider_manager.configured_providers
338
+ if configured.nil? || configured.empty?
339
+ raise ConversationError, <<~MSG.strip
340
+ No providers are configured. Create an aidp.yml with at least one provider, for example:
341
+
342
+ harness:\n enabled: true\n default_provider: claude\nproviders:\n claude:\n type: api\n api_key: "${AIDP_CLAUDE_API_KEY}"\n models:\n - claude-3-5-sonnet-20241022
343
+ MSG
344
+ end
345
+
346
+ default = @provider_manager.current_provider
347
+ unless default && configured.include?(default)
348
+ raise ConversationError, "Default provider '#{default || "(nil)"}' not found in configured providers: #{configured.join(", ")}"
349
+ end
350
+ end
351
+
279
352
  def present_recommendation(recommendation)
280
353
  display_message("\n✨ Recommendation", type: :highlight)
281
354
  display_message("─" * 60, type: :muted)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aidp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.1
4
+ version: 0.14.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan
@@ -259,7 +259,6 @@ files:
259
259
  - lib/aidp/core_ext/class_attribute.rb
260
260
  - lib/aidp/daemon/process_manager.rb
261
261
  - lib/aidp/daemon/runner.rb
262
- - lib/aidp/debug_logger.rb
263
262
  - lib/aidp/debug_mixin.rb
264
263
  - lib/aidp/execute/async_work_loop_runner.rb
265
264
  - lib/aidp/execute/checkpoint.rb
@@ -337,6 +336,7 @@ files:
337
336
  - lib/aidp/providers/github_copilot.rb
338
337
  - lib/aidp/providers/macos_ui.rb
339
338
  - lib/aidp/providers/opencode.rb
339
+ - lib/aidp/rescue_logging.rb
340
340
  - lib/aidp/setup/wizard.rb
341
341
  - lib/aidp/storage/csv_storage.rb
342
342
  - lib/aidp/storage/file_manager.rb
@@ -1,195 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "json"
5
- require "pastel"
6
-
7
- module Aidp
8
- # Debug logger that outputs to both console and a single log file
9
- class DebugLogger
10
- LOG_LEVELS = {
11
- debug: 0,
12
- info: 1,
13
- warn: 2,
14
- error: 3
15
- }.freeze
16
-
17
- def initialize(log_dir: nil)
18
- @log_dir = log_dir || default_log_dir
19
- @log_file = nil
20
- @run_started = false
21
- ensure_log_directory
22
- log_run_banner
23
- end
24
-
25
- def log(message, level: :info, data: nil)
26
- timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")
27
- level_str = level.to_s.upcase.ljust(5)
28
-
29
- # Format message with timestamp and level
30
- formatted_message = "[#{timestamp}] #{level_str} #{message}"
31
-
32
- # Add data if present
33
- if data && !data.empty?
34
- data_str = format_data(data)
35
- formatted_message += "\n#{data_str}" if data_str
36
- end
37
-
38
- # Output to console
39
- output_to_console(formatted_message, level)
40
-
41
- # Output to log file
42
- output_to_file(formatted_message, level, data)
43
- end
44
-
45
- def close
46
- @log_file&.close
47
- @log_file = nil
48
- end
49
-
50
- private
51
-
52
- def default_log_dir
53
- File.join(Dir.pwd, ".aidp", "debug_logs")
54
- end
55
-
56
- def ensure_log_directory
57
- FileUtils.mkdir_p(@log_dir) unless Dir.exist?(@log_dir)
58
- end
59
-
60
- def log_file_path
61
- @log_file_path ||= File.join(@log_dir, "aidp_debug.log")
62
- end
63
-
64
- def get_log_file
65
- return @log_file if @log_file && !@log_file.closed?
66
-
67
- @log_file = File.open(log_file_path, "a")
68
- @log_file
69
- end
70
-
71
- def log_run_banner
72
- return if @run_started
73
- @run_started = true
74
-
75
- timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")
76
- command_line = build_command_line
77
-
78
- banner = <<~BANNER
79
-
80
- ================================================================================
81
- AIDP DEBUG SESSION STARTED
82
- ================================================================================
83
- Timestamp: #{timestamp}
84
- Command: #{command_line}
85
- Working Directory: #{Dir.pwd}
86
- Debug Level: #{ENV["DEBUG"] || "0"}
87
- ================================================================================
88
- BANNER
89
-
90
- # Write banner to log file
91
- file = get_log_file
92
- file.puts banner
93
- file.flush
94
-
95
- # Also output to console if debug is enabled
96
- if ENV["DEBUG"] && ENV["DEBUG"].to_i > 0
97
- puts "\e[36m#{banner}\e[0m" # Cyan color
98
- end
99
- end
100
-
101
- def build_command_line
102
- # Get the command line arguments
103
- cmd_parts = []
104
-
105
- # Add the main command (aidp)
106
- cmd_parts << "aidp"
107
-
108
- # Add any arguments from ARGV
109
- if defined?(ARGV) && !ARGV.empty?
110
- cmd_parts.concat(ARGV)
111
- end
112
-
113
- # Add environment variables that affect behavior
114
- env_vars = []
115
- env_vars << "DEBUG=#{ENV["DEBUG"]}" if ENV["DEBUG"]
116
- env_vars << "AIDP_CONFIG=#{ENV["AIDP_CONFIG"]}" if ENV["AIDP_CONFIG"]
117
-
118
- if env_vars.any?
119
- cmd_parts << "(" + env_vars.join(" ") + ")"
120
- end
121
-
122
- cmd_parts.join(" ")
123
- end
124
-
125
- def output_to_console(message, level)
126
- case level
127
- when :error
128
- warn message
129
- when :warn
130
- puts "\e[33m#{message}\e[0m" # Yellow
131
- when :info
132
- puts "\e[36m#{message}\e[0m" # Cyan
133
- when :debug
134
- puts "\e[90m#{message}\e[0m" # Gray
135
- else
136
- puts message
137
- end
138
- end
139
-
140
- def output_to_file(message, level, data)
141
- file = get_log_file
142
- file.puts message
143
-
144
- # Add structured data if present
145
- if data && !data.empty?
146
- structured_data = {
147
- timestamp: Time.now.strftime("%Y-%m-%dT%H:%M:%S.%3N%z"),
148
- level: level.to_s,
149
- message: message,
150
- data: data
151
- }
152
- file.puts "DATA: #{JSON.generate(structured_data)}"
153
- end
154
-
155
- file.flush
156
- end
157
-
158
- def format_data(data)
159
- return nil if data.nil? || data.empty?
160
-
161
- case data
162
- when Hash
163
- format_hash_data(data)
164
- when Array
165
- format_array_data(data)
166
- when String
167
- (data.length > 200) ? "#{data[0..200]}..." : data
168
- else
169
- data.to_s
170
- end
171
- end
172
-
173
- def format_hash_data(hash)
174
- lines = []
175
- hash.each do |key, value|
176
- lines << if value.is_a?(String) && value.length > 100
177
- " #{key}: #{value[0..100]}..."
178
- elsif value.is_a?(Hash) || value.is_a?(Array)
179
- " #{key}: #{JSON.pretty_generate(value).gsub("\n", "\n ")}"
180
- else
181
- " #{key}: #{value}"
182
- end
183
- end
184
- lines.join("\n")
185
- end
186
-
187
- def format_array_data(array)
188
- if array.length > 10
189
- "#{array.first(5).join(", ")}... (#{array.length} total items)"
190
- else
191
- array.join(", ")
192
- end
193
- end
194
- end
195
- end