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.
- checksums.yaml +4 -4
- data/lib/aidp/cli.rb +33 -0
- data/lib/aidp/debug_mixin.rb +34 -33
- data/lib/aidp/execute/async_work_loop_runner.rb +5 -0
- data/lib/aidp/execute/checkpoint.rb +28 -5
- data/lib/aidp/execute/interactive_repl.rb +7 -0
- data/lib/aidp/execute/work_loop_runner.rb +8 -1
- data/lib/aidp/harness/enhanced_runner.rb +2 -0
- data/lib/aidp/harness/provider_info.rb +14 -4
- data/lib/aidp/harness/provider_manager.rb +64 -12
- data/lib/aidp/jobs/background_runner.rb +10 -3
- data/lib/aidp/logger.rb +10 -71
- data/lib/aidp/providers/base.rb +2 -0
- data/lib/aidp/providers/github_copilot.rb +12 -0
- data/lib/aidp/rescue_logging.rb +36 -0
- data/lib/aidp/setup/wizard.rb +42 -46
- data/lib/aidp/storage/csv_storage.rb +33 -7
- data/lib/aidp/storage/json_storage.rb +33 -10
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/guided_agent.rb +95 -22
- metadata +2 -2
- data/lib/aidp/debug_logger.rb +0 -195
@@ -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
|
-
|
38
|
-
|
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
|
-
|
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
|
-
|
78
|
-
|
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
|
-
|
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
@@ -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:
|
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
|
-
|
237
|
-
|
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
|
-
|
240
|
-
|
241
|
-
end
|
278
|
+
begin
|
279
|
+
attempts += 1
|
242
280
|
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
-
|
248
|
-
|
249
|
-
|
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
|
-
|
252
|
-
|
290
|
+
unless provider
|
291
|
+
raise ConversationError, "Failed to create provider instance for #{provider_name}"
|
292
|
+
end
|
253
293
|
|
254
|
-
|
294
|
+
combined_prompt = "#{system_prompt}\n\n#{user_prompt}"
|
295
|
+
result = provider.send(prompt: combined_prompt)
|
255
296
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
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
|
-
|
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.
|
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
|
data/lib/aidp/debug_logger.rb
DELETED
@@ -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
|