aidp 0.10.0 → 0.11.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 +194 -25
- data/lib/aidp/analyze/kb_inspector.rb +2 -15
- data/lib/aidp/analyze/progress.rb +2 -1
- data/lib/aidp/analyze/ruby_maat_integration.rb +2 -15
- data/lib/aidp/analyze/runner.rb +64 -20
- data/lib/aidp/analyze/steps.rb +10 -8
- data/lib/aidp/analyze/tree_sitter_grammar_loader.rb +2 -13
- data/lib/aidp/analyze/tree_sitter_scan.rb +2 -13
- data/lib/aidp/cli/checkpoint_command.rb +98 -0
- data/lib/aidp/cli/first_run_wizard.rb +65 -94
- data/lib/aidp/cli/jobs_command.rb +249 -34
- data/lib/aidp/cli.rb +312 -38
- data/lib/aidp/config.rb +5 -8
- data/lib/aidp/debug_logger.rb +4 -4
- data/lib/aidp/debug_mixin.rb +11 -4
- data/lib/aidp/execute/checkpoint.rb +282 -0
- data/lib/aidp/execute/checkpoint_display.rb +221 -0
- data/lib/aidp/execute/progress.rb +2 -1
- data/lib/aidp/execute/prompt_manager.rb +62 -0
- data/lib/aidp/execute/runner.rb +53 -24
- data/lib/aidp/execute/steps.rb +36 -27
- data/lib/aidp/execute/work_loop_runner.rb +308 -0
- data/lib/aidp/execute/workflow_selector.rb +26 -17
- data/lib/aidp/harness/condition_detector.rb +4 -4
- data/lib/aidp/harness/config_schema.rb +40 -0
- data/lib/aidp/harness/config_validator.rb +3 -6
- data/lib/aidp/harness/configuration.rb +35 -1
- data/lib/aidp/harness/enhanced_runner.rb +22 -1
- data/lib/aidp/harness/error_handler.rb +103 -28
- data/lib/aidp/harness/provider_factory.rb +4 -1
- data/lib/aidp/harness/provider_manager.rb +250 -15
- data/lib/aidp/harness/runner.rb +3 -14
- data/lib/aidp/harness/simple_user_interface.rb +2 -15
- data/lib/aidp/harness/status_display.rb +12 -17
- data/lib/aidp/harness/test_runner.rb +83 -0
- data/lib/aidp/harness/ui/enhanced_tui.rb +2 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +22 -4
- data/lib/aidp/harness/ui/error_handler.rb +4 -0
- data/lib/aidp/harness/ui/frame_manager.rb +10 -8
- data/lib/aidp/harness/ui/job_monitor.rb +2 -0
- data/lib/aidp/harness/ui/navigation/main_menu.rb +4 -2
- data/lib/aidp/harness/ui/navigation/menu_item.rb +1 -0
- data/lib/aidp/harness/ui/navigation/menu_state.rb +1 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +2 -0
- data/lib/aidp/harness/ui/progress_display.rb +8 -12
- data/lib/aidp/harness/ui/question_collector.rb +2 -0
- data/lib/aidp/harness/ui/spinner_group.rb +2 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +1 -1
- data/lib/aidp/harness/ui/status_manager.rb +4 -2
- data/lib/aidp/harness/ui/status_widget.rb +3 -1
- data/lib/aidp/harness/ui/workflow_controller.rb +2 -0
- data/lib/aidp/harness/user_interface.rb +12 -17
- data/lib/aidp/jobs/background_runner.rb +278 -0
- data/lib/aidp/message_display.rb +48 -0
- data/lib/aidp/provider_manager.rb +3 -1
- data/lib/aidp/providers/anthropic.rb +100 -17
- data/lib/aidp/providers/base.rb +42 -11
- data/lib/aidp/providers/codex.rb +248 -0
- data/lib/aidp/providers/cursor.rb +35 -42
- data/lib/aidp/providers/gemini.rb +25 -15
- data/lib/aidp/providers/github_copilot.rb +41 -42
- data/lib/aidp/providers/opencode.rb +34 -41
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/definitions.rb +357 -0
- data/lib/aidp/workflows/selector.rb +171 -0
- data/lib/aidp.rb +12 -0
- data/templates/planning/generate_llm_style_guide.md +119 -0
- metadata +38 -26
- /data/templates/{ANALYZE/02_ARCHITECTURE_ANALYSIS.md → analysis/analyze_architecture.md} +0 -0
- /data/templates/{ANALYZE/05_DOCUMENTATION_ANALYSIS.md → analysis/analyze_documentation.md} +0 -0
- /data/templates/{ANALYZE/04_FUNCTIONALITY_ANALYSIS.md → analysis/analyze_functionality.md} +0 -0
- /data/templates/{ANALYZE/01_REPOSITORY_ANALYSIS.md → analysis/analyze_repository.md} +0 -0
- /data/templates/{ANALYZE/06_STATIC_ANALYSIS.md → analysis/analyze_static_code.md} +0 -0
- /data/templates/{ANALYZE/03_TEST_ANALYSIS.md → analysis/analyze_tests.md} +0 -0
- /data/templates/{ANALYZE/07_REFACTORING_RECOMMENDATIONS.md → analysis/recommend_refactoring.md} +0 -0
- /data/templates/{ANALYZE/06a_tree_sitter_scan.md → analysis/scan_with_tree_sitter.md} +0 -0
- /data/templates/{EXECUTE/11_STATIC_ANALYSIS.md → implementation/configure_static_analysis.md} +0 -0
- /data/templates/{EXECUTE/14_DOCS_PORTAL.md → implementation/create_documentation_portal.md} +0 -0
- /data/templates/{EXECUTE/10_IMPLEMENTATION_AGENT.md → implementation/implement_features.md} +0 -0
- /data/templates/{EXECUTE/13_DELIVERY_ROLLOUT.md → implementation/plan_delivery.md} +0 -0
- /data/templates/{EXECUTE/15_POST_RELEASE.md → implementation/review_post_release.md} +0 -0
- /data/templates/{EXECUTE/09_SCAFFOLDING_DEVEX.md → implementation/setup_scaffolding.md} +0 -0
- /data/templates/{EXECUTE/02A_ARCH_GATE_QUESTIONS.md → planning/ask_architecture_questions.md} +0 -0
- /data/templates/{EXECUTE/00_PRD.md → planning/create_prd.md} +0 -0
- /data/templates/{EXECUTE/08_TASKS.md → planning/create_tasks.md} +0 -0
- /data/templates/{EXECUTE/04_DOMAIN_DECOMPOSITION.md → planning/decompose_domain.md} +0 -0
- /data/templates/{EXECUTE/01_NFRS.md → planning/define_nfrs.md} +0 -0
- /data/templates/{EXECUTE/05_CONTRACTS.md → planning/design_apis.md} +0 -0
- /data/templates/{EXECUTE/02_ARCHITECTURE.md → planning/design_architecture.md} +0 -0
- /data/templates/{EXECUTE/06_THREAT_MODEL.md → planning/design_data_model.md} +0 -0
- /data/templates/{EXECUTE/03_ADR_FACTORY.md → planning/generate_adrs.md} +0 -0
- /data/templates/{EXECUTE/12_OBSERVABILITY_SLOS.md → planning/plan_observability.md} +0 -0
- /data/templates/{EXECUTE/07_TEST_PLAN.md → planning/plan_testing.md} +0 -0
@@ -0,0 +1,278 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
require "yaml"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
module Aidp
|
8
|
+
module Jobs
|
9
|
+
# Manages background execution of work loops
|
10
|
+
# Runs harness in daemon process and tracks job metadata
|
11
|
+
class BackgroundRunner
|
12
|
+
include Aidp::MessageDisplay
|
13
|
+
|
14
|
+
attr_reader :project_dir, :jobs_dir
|
15
|
+
|
16
|
+
def initialize(project_dir = Dir.pwd)
|
17
|
+
@project_dir = project_dir
|
18
|
+
@jobs_dir = File.join(project_dir, ".aidp", "jobs")
|
19
|
+
ensure_jobs_directory
|
20
|
+
end
|
21
|
+
|
22
|
+
# Start a background job
|
23
|
+
# Returns job_id
|
24
|
+
def start(mode, options = {})
|
25
|
+
job_id = generate_job_id
|
26
|
+
log_file = File.join(@jobs_dir, job_id, "output.log")
|
27
|
+
pid_file = File.join(@jobs_dir, job_id, "job.pid")
|
28
|
+
|
29
|
+
# Create job directory
|
30
|
+
FileUtils.mkdir_p(File.dirname(log_file))
|
31
|
+
|
32
|
+
# Fork and daemonize
|
33
|
+
pid = fork do
|
34
|
+
# Detach from parent process
|
35
|
+
Process.daemon(true)
|
36
|
+
|
37
|
+
# Redirect stdout/stderr to log file
|
38
|
+
$stdout.reopen(log_file, "a")
|
39
|
+
$stderr.reopen(log_file, "a")
|
40
|
+
$stdout.sync = true
|
41
|
+
$stderr.sync = true
|
42
|
+
|
43
|
+
# Write PID file
|
44
|
+
File.write(pid_file, Process.pid)
|
45
|
+
|
46
|
+
begin
|
47
|
+
# Run the harness
|
48
|
+
puts "[#{Time.now}] Starting #{mode} mode in background"
|
49
|
+
puts "[#{Time.now}] Job ID: #{job_id}"
|
50
|
+
puts "[#{Time.now}] PID: #{Process.pid}"
|
51
|
+
|
52
|
+
runner = Aidp::Harness::Runner.new(@project_dir, mode, options.merge(job_id: job_id))
|
53
|
+
result = runner.run
|
54
|
+
|
55
|
+
puts "[#{Time.now}] Job completed with status: #{result[:status]}"
|
56
|
+
mark_job_completed(job_id, result)
|
57
|
+
rescue => e
|
58
|
+
puts "[#{Time.now}] Job failed with error: #{e.message}"
|
59
|
+
puts e.backtrace.join("\n")
|
60
|
+
mark_job_failed(job_id, e)
|
61
|
+
ensure
|
62
|
+
# Clean up PID file
|
63
|
+
File.delete(pid_file) if File.exist?(pid_file)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Wait for child to fork
|
68
|
+
Process.detach(pid)
|
69
|
+
sleep 0.1 # Give daemon time to write PID file
|
70
|
+
|
71
|
+
# Save job metadata in parent process
|
72
|
+
save_job_metadata(job_id, pid, mode, options)
|
73
|
+
|
74
|
+
job_id
|
75
|
+
end
|
76
|
+
|
77
|
+
# List all jobs
|
78
|
+
def list_jobs
|
79
|
+
return [] unless Dir.exist?(@jobs_dir)
|
80
|
+
|
81
|
+
Dir.glob(File.join(@jobs_dir, "*")).select { |d| File.directory?(d) }.map do |job_dir|
|
82
|
+
job_id = File.basename(job_dir)
|
83
|
+
load_job_metadata(job_id)
|
84
|
+
end.compact.sort_by { |job| job[:started_at] || Time.now }.reverse
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get job status
|
88
|
+
def job_status(job_id)
|
89
|
+
metadata = load_job_metadata(job_id)
|
90
|
+
return nil unless metadata
|
91
|
+
|
92
|
+
# Check if process is still running
|
93
|
+
pid = metadata[:pid]
|
94
|
+
running = pid && process_running?(pid)
|
95
|
+
|
96
|
+
# Get checkpoint data
|
97
|
+
checkpoint = get_job_checkpoint(job_id)
|
98
|
+
|
99
|
+
{
|
100
|
+
job_id: job_id,
|
101
|
+
mode: metadata[:mode],
|
102
|
+
status: determine_job_status(metadata, running, checkpoint),
|
103
|
+
pid: pid,
|
104
|
+
running: running,
|
105
|
+
started_at: metadata[:started_at],
|
106
|
+
completed_at: metadata[:completed_at],
|
107
|
+
checkpoint: checkpoint,
|
108
|
+
log_file: File.join(@jobs_dir, job_id, "output.log")
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
# Stop a running job
|
113
|
+
def stop_job(job_id)
|
114
|
+
metadata = load_job_metadata(job_id)
|
115
|
+
return {success: false, message: "Job not found"} unless metadata
|
116
|
+
|
117
|
+
pid = metadata[:pid]
|
118
|
+
unless pid && process_running?(pid)
|
119
|
+
return {success: false, message: "Job is not running"}
|
120
|
+
end
|
121
|
+
|
122
|
+
begin
|
123
|
+
# Send TERM signal
|
124
|
+
Process.kill("TERM", pid)
|
125
|
+
|
126
|
+
# Wait for process to terminate (max 10 seconds)
|
127
|
+
10.times do
|
128
|
+
sleep 0.5
|
129
|
+
break unless process_running?(pid)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Force kill if still running
|
133
|
+
if process_running?(pid)
|
134
|
+
Process.kill("KILL", pid)
|
135
|
+
end
|
136
|
+
|
137
|
+
mark_job_stopped(job_id)
|
138
|
+
{success: true, message: "Job stopped successfully"}
|
139
|
+
rescue Errno::ESRCH
|
140
|
+
# Process already dead
|
141
|
+
mark_job_stopped(job_id)
|
142
|
+
{success: true, message: "Job was already stopped"}
|
143
|
+
rescue => e
|
144
|
+
{success: false, message: "Failed to stop job: #{e.message}"}
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Get job logs
|
149
|
+
def job_logs(job_id, options = {})
|
150
|
+
log_file = File.join(@jobs_dir, job_id, "output.log")
|
151
|
+
return nil unless File.exist?(log_file)
|
152
|
+
|
153
|
+
if options[:tail]
|
154
|
+
lines = options[:lines] || 50
|
155
|
+
`tail -n #{lines} #{log_file}`
|
156
|
+
else
|
157
|
+
File.read(log_file)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Follow job logs in real-time
|
162
|
+
def follow_job_logs(job_id)
|
163
|
+
log_file = File.join(@jobs_dir, job_id, "output.log")
|
164
|
+
return unless File.exist?(log_file)
|
165
|
+
|
166
|
+
# Use tail -f to follow logs
|
167
|
+
exec("tail", "-f", log_file)
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def ensure_jobs_directory
|
173
|
+
FileUtils.mkdir_p(@jobs_dir) unless Dir.exist?(@jobs_dir)
|
174
|
+
end
|
175
|
+
|
176
|
+
def generate_job_id
|
177
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
178
|
+
random = SecureRandom.hex(4)
|
179
|
+
"#{timestamp}_#{random}"
|
180
|
+
end
|
181
|
+
|
182
|
+
def save_job_metadata(job_id, pid, mode, options)
|
183
|
+
metadata_file = File.join(@jobs_dir, job_id, "metadata.yml")
|
184
|
+
|
185
|
+
metadata = {
|
186
|
+
job_id: job_id,
|
187
|
+
pid: pid,
|
188
|
+
mode: mode,
|
189
|
+
started_at: Time.now,
|
190
|
+
status: "running",
|
191
|
+
options: options.except(:prompt) # Don't save prompt object
|
192
|
+
}
|
193
|
+
|
194
|
+
File.write(metadata_file, metadata.to_yaml)
|
195
|
+
end
|
196
|
+
|
197
|
+
def load_job_metadata(job_id)
|
198
|
+
metadata_file = File.join(@jobs_dir, job_id, "metadata.yml")
|
199
|
+
return nil unless File.exist?(metadata_file)
|
200
|
+
|
201
|
+
YAML.load_file(metadata_file)
|
202
|
+
rescue
|
203
|
+
nil
|
204
|
+
end
|
205
|
+
|
206
|
+
def update_job_metadata(job_id, updates)
|
207
|
+
metadata = load_job_metadata(job_id)
|
208
|
+
return unless metadata
|
209
|
+
|
210
|
+
metadata.merge!(updates)
|
211
|
+
metadata_file = File.join(@jobs_dir, job_id, "metadata.yml")
|
212
|
+
File.write(metadata_file, metadata.to_yaml)
|
213
|
+
end
|
214
|
+
|
215
|
+
def mark_job_completed(job_id, result)
|
216
|
+
update_job_metadata(job_id, {
|
217
|
+
status: "completed",
|
218
|
+
completed_at: Time.now,
|
219
|
+
result: result
|
220
|
+
})
|
221
|
+
end
|
222
|
+
|
223
|
+
def mark_job_failed(job_id, error)
|
224
|
+
update_job_metadata(job_id, {
|
225
|
+
status: "failed",
|
226
|
+
completed_at: Time.now,
|
227
|
+
error: {
|
228
|
+
message: error.message,
|
229
|
+
class: error.class.name,
|
230
|
+
backtrace: error.backtrace&.first(10)
|
231
|
+
}
|
232
|
+
})
|
233
|
+
end
|
234
|
+
|
235
|
+
def mark_job_stopped(job_id)
|
236
|
+
update_job_metadata(job_id, {
|
237
|
+
status: "stopped",
|
238
|
+
completed_at: Time.now
|
239
|
+
})
|
240
|
+
end
|
241
|
+
|
242
|
+
def process_running?(pid)
|
243
|
+
return false unless pid
|
244
|
+
|
245
|
+
Process.kill(0, pid)
|
246
|
+
true
|
247
|
+
rescue Errno::ESRCH, Errno::EPERM
|
248
|
+
false
|
249
|
+
end
|
250
|
+
|
251
|
+
def get_job_checkpoint(job_id)
|
252
|
+
# Try to load checkpoint from project directory
|
253
|
+
checkpoint = Aidp::Execute::Checkpoint.new(@project_dir)
|
254
|
+
checkpoint.latest_checkpoint
|
255
|
+
rescue
|
256
|
+
nil
|
257
|
+
end
|
258
|
+
|
259
|
+
def determine_job_status(metadata, running, checkpoint)
|
260
|
+
return metadata[:status] if metadata[:status] && !%w[running].include?(metadata[:status])
|
261
|
+
|
262
|
+
if running
|
263
|
+
# Check if job appears stuck based on checkpoint
|
264
|
+
if checkpoint && checkpoint[:timestamp]
|
265
|
+
last_update = Time.parse(checkpoint[:timestamp])
|
266
|
+
age = Time.now - last_update
|
267
|
+
|
268
|
+
return "stuck" if age > 600 # No update in 10 minutes
|
269
|
+
return "running"
|
270
|
+
end
|
271
|
+
"running"
|
272
|
+
else
|
273
|
+
metadata[:status] || "completed"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-prompt"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
# Mixin providing a consistent display_message helper across classes.
|
7
|
+
# Usage:
|
8
|
+
# include Aidp::MessageDisplay
|
9
|
+
# display_message("Hello", type: :success)
|
10
|
+
# Supports color types: :error, :success, :warning, :info, :highlight, :muted
|
11
|
+
module MessageDisplay
|
12
|
+
COLOR_MAP = {
|
13
|
+
error: :red,
|
14
|
+
success: :green,
|
15
|
+
warning: :yellow,
|
16
|
+
warn: :yellow,
|
17
|
+
info: :blue,
|
18
|
+
highlight: :cyan,
|
19
|
+
muted: :bright_black
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
def self.included(base)
|
23
|
+
base.extend(ClassMethods)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Instance helper for displaying a colored message via TTY::Prompt
|
27
|
+
def display_message(message, type: :info)
|
28
|
+
prompt = message_display_prompt
|
29
|
+
prompt.say(message, color: COLOR_MAP.fetch(type, :white))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Provide a memoized prompt per including instance (if it defines @prompt)
|
33
|
+
def message_display_prompt
|
34
|
+
if instance_variable_defined?(:@prompt) && @prompt
|
35
|
+
@prompt
|
36
|
+
else
|
37
|
+
@__message_display_prompt ||= TTY::Prompt.new
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
# Class-level display helper (stateless)
|
43
|
+
def display_message(message, type: :info)
|
44
|
+
TTY::Prompt.new.say(message, color: COLOR_MAP.fetch(type, :white))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -138,7 +138,7 @@ module Aidp
|
|
138
138
|
case provider_type
|
139
139
|
when "cursor"
|
140
140
|
Aidp::Providers::Cursor.new(prompt: prompt)
|
141
|
-
when "anthropic"
|
141
|
+
when "anthropic", "claude"
|
142
142
|
Aidp::Providers::Anthropic.new(prompt: prompt)
|
143
143
|
when "gemini"
|
144
144
|
Aidp::Providers::Gemini.new(prompt: prompt)
|
@@ -146,6 +146,8 @@ module Aidp
|
|
146
146
|
Aidp::Providers::MacOSUI.new(prompt: prompt)
|
147
147
|
when "github_copilot"
|
148
148
|
Aidp::Providers::GithubCopilot.new(prompt: prompt)
|
149
|
+
when "codex"
|
150
|
+
Aidp::Providers::Codex.new(prompt: prompt)
|
149
151
|
end
|
150
152
|
end
|
151
153
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "json"
|
3
4
|
require_relative "base"
|
4
5
|
require_relative "../debug_mixin"
|
5
6
|
|
@@ -16,6 +17,10 @@ module Aidp
|
|
16
17
|
"anthropic"
|
17
18
|
end
|
18
19
|
|
20
|
+
def display_name
|
21
|
+
"Anthropic Claude CLI"
|
22
|
+
end
|
23
|
+
|
19
24
|
def available?
|
20
25
|
self.class.available?
|
21
26
|
end
|
@@ -29,16 +34,46 @@ module Aidp
|
|
29
34
|
debug_provider("claude", "Starting execution", {timeout: timeout_seconds})
|
30
35
|
debug_log("📝 Sending prompt to claude...", level: :info)
|
31
36
|
|
37
|
+
# Check if streaming mode is enabled
|
38
|
+
streaming_enabled = ENV["AIDP_STREAMING"] == "1" || ENV["DEBUG"] == "1"
|
39
|
+
|
40
|
+
# Build command arguments with proper streaming support
|
41
|
+
args = ["--print"]
|
42
|
+
if streaming_enabled
|
43
|
+
# Claude CLI requires --verbose when using --print with --output-format=stream-json
|
44
|
+
args += ["--verbose", "--output-format=stream-json", "--include-partial-messages"]
|
45
|
+
display_message("📺 True streaming enabled - real-time chunks from Claude API", type: :info)
|
46
|
+
else
|
47
|
+
# Use text format for non-streaming (default behavior)
|
48
|
+
args += ["--output-format=text"]
|
49
|
+
end
|
50
|
+
|
32
51
|
begin
|
33
|
-
# Use debug_execute_command
|
34
|
-
result = debug_execute_command("claude", args:
|
52
|
+
# Use debug_execute_command with streaming support
|
53
|
+
result = debug_execute_command("claude", args: args, input: prompt, timeout: timeout_seconds, streaming: streaming_enabled)
|
35
54
|
|
36
55
|
# Log the results
|
37
|
-
debug_command("claude", args:
|
56
|
+
debug_command("claude", args: args, input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
|
38
57
|
|
39
58
|
if result.exit_status == 0
|
40
|
-
|
59
|
+
# Handle different output formats
|
60
|
+
if streaming_enabled && args.include?("--output-format=stream-json")
|
61
|
+
# Parse stream-json output and extract final content
|
62
|
+
parse_stream_json_output(result.out)
|
63
|
+
else
|
64
|
+
# Return text output as-is
|
65
|
+
result.out
|
66
|
+
end
|
41
67
|
else
|
68
|
+
# Detect auth issues in stdout/stderr (Claude sometimes prints JSON with auth error to stdout)
|
69
|
+
combined = [result.out, result.err].compact.join("\n")
|
70
|
+
if combined.downcase.include?("oauth token has expired") || combined.downcase.include?("authentication_error")
|
71
|
+
error_message = "Authentication error from Claude CLI: token expired or invalid. Run 'claude /login' or refresh credentials."
|
72
|
+
debug_error(StandardError.new(error_message), {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
73
|
+
# Raise a recognizable error for classifier
|
74
|
+
raise error_message
|
75
|
+
end
|
76
|
+
|
42
77
|
debug_error(StandardError.new("claude failed"), {exit_code: result.exit_status, stderr: result.err})
|
43
78
|
raise "claude failed with exit code #{result.exit_status}: #{result.err}"
|
44
79
|
end
|
@@ -58,8 +93,8 @@ module Aidp
|
|
58
93
|
# 4. Default timeout
|
59
94
|
|
60
95
|
if ENV["AIDP_QUICK_MODE"]
|
61
|
-
display_message("⚡ Quick mode enabled -
|
62
|
-
return
|
96
|
+
display_message("⚡ Quick mode enabled - #{TIMEOUT_QUICK_MODE / 60} minute timeout", type: :highlight)
|
97
|
+
return TIMEOUT_QUICK_MODE
|
63
98
|
end
|
64
99
|
|
65
100
|
if ENV["AIDP_ANTHROPIC_TIMEOUT"]
|
@@ -73,9 +108,9 @@ module Aidp
|
|
73
108
|
return step_timeout
|
74
109
|
end
|
75
110
|
|
76
|
-
# Default timeout
|
77
|
-
display_message("📋 Using default timeout:
|
78
|
-
|
111
|
+
# Default timeout
|
112
|
+
display_message("📋 Using default timeout: #{TIMEOUT_DEFAULT / 60} minutes", type: :info)
|
113
|
+
TIMEOUT_DEFAULT
|
79
114
|
end
|
80
115
|
|
81
116
|
def get_adaptive_timeout
|
@@ -84,23 +119,71 @@ module Aidp
|
|
84
119
|
|
85
120
|
case step_name
|
86
121
|
when /REPOSITORY_ANALYSIS/
|
87
|
-
|
122
|
+
TIMEOUT_REPOSITORY_ANALYSIS
|
88
123
|
when /ARCHITECTURE_ANALYSIS/
|
89
|
-
|
124
|
+
TIMEOUT_ARCHITECTURE_ANALYSIS
|
90
125
|
when /TEST_ANALYSIS/
|
91
|
-
|
126
|
+
TIMEOUT_TEST_ANALYSIS
|
92
127
|
when /FUNCTIONALITY_ANALYSIS/
|
93
|
-
|
128
|
+
TIMEOUT_FUNCTIONALITY_ANALYSIS
|
94
129
|
when /DOCUMENTATION_ANALYSIS/
|
95
|
-
|
130
|
+
TIMEOUT_DOCUMENTATION_ANALYSIS
|
96
131
|
when /STATIC_ANALYSIS/
|
97
|
-
|
132
|
+
TIMEOUT_STATIC_ANALYSIS
|
98
133
|
when /REFACTORING_RECOMMENDATIONS/
|
99
|
-
|
134
|
+
TIMEOUT_REFACTORING_RECOMMENDATIONS
|
100
135
|
else
|
101
|
-
nil
|
136
|
+
nil # Use default
|
102
137
|
end
|
103
138
|
end
|
139
|
+
|
140
|
+
# Parse stream-json output from Claude CLI
|
141
|
+
def parse_stream_json_output(output)
|
142
|
+
return output if output.nil? || output.empty?
|
143
|
+
|
144
|
+
# Stream-json output contains multiple JSON objects, one per line
|
145
|
+
# We want to extract the final content from the last complete message
|
146
|
+
lines = output.strip.split("\n")
|
147
|
+
content_parts = []
|
148
|
+
|
149
|
+
lines.each do |line|
|
150
|
+
next if line.strip.empty?
|
151
|
+
|
152
|
+
begin
|
153
|
+
json_obj = JSON.parse(line)
|
154
|
+
|
155
|
+
# Look for content in various possible structures
|
156
|
+
if json_obj["type"] == "content_block_delta" && json_obj["delta"] && json_obj["delta"]["text"]
|
157
|
+
content_parts << json_obj["delta"]["text"]
|
158
|
+
elsif json_obj["content"]&.is_a?(Array)
|
159
|
+
json_obj["content"].each do |content_item|
|
160
|
+
content_parts << content_item["text"] if content_item["text"]
|
161
|
+
end
|
162
|
+
elsif json_obj["message"] && json_obj["message"]["content"]
|
163
|
+
if json_obj["message"]["content"].is_a?(Array)
|
164
|
+
json_obj["message"]["content"].each do |content_item|
|
165
|
+
content_parts << content_item["text"] if content_item["text"]
|
166
|
+
end
|
167
|
+
elsif json_obj["message"]["content"].is_a?(String)
|
168
|
+
content_parts << json_obj["message"]["content"]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
rescue JSON::ParserError => e
|
172
|
+
debug_log("⚠️ Failed to parse JSON line: #{e.message}", level: :warn, data: {line: line})
|
173
|
+
# If JSON parsing fails, treat as plain text
|
174
|
+
content_parts << line
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
result = content_parts.join
|
179
|
+
|
180
|
+
# Fallback: if no content found in JSON, return original output
|
181
|
+
result.empty? ? output : result
|
182
|
+
rescue => e
|
183
|
+
debug_log("⚠️ Failed to parse stream-json output: #{e.message}", level: :warn)
|
184
|
+
# Return original output if parsing fails
|
185
|
+
output
|
186
|
+
end
|
104
187
|
end
|
105
188
|
end
|
106
189
|
end
|
data/lib/aidp/providers/base.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "tty-prompt"
|
4
|
+
require "tty-spinner"
|
4
5
|
|
5
6
|
module Aidp
|
6
7
|
module Providers
|
7
8
|
class Base
|
9
|
+
include Aidp::MessageDisplay
|
10
|
+
|
8
11
|
# Activity indicator states
|
9
12
|
ACTIVITY_STATES = {
|
10
13
|
idle: "⏳",
|
@@ -17,6 +20,18 @@ module Aidp
|
|
17
20
|
# Default timeout for stuck detection (2 minutes)
|
18
21
|
DEFAULT_STUCK_TIMEOUT = 120
|
19
22
|
|
23
|
+
# Configurable timeout values (can be overridden via environment or config)
|
24
|
+
# These defaults provide reasonable values for different execution scenarios
|
25
|
+
TIMEOUT_QUICK_MODE = 120 # 2 minutes - for quick testing
|
26
|
+
TIMEOUT_DEFAULT = 300 # 5 minutes - standard interactive timeout
|
27
|
+
TIMEOUT_REPOSITORY_ANALYSIS = 180 # 3 minutes - repository analysis
|
28
|
+
TIMEOUT_ARCHITECTURE_ANALYSIS = 600 # 10 minutes - architecture analysis
|
29
|
+
TIMEOUT_TEST_ANALYSIS = 300 # 5 minutes - test analysis
|
30
|
+
TIMEOUT_FUNCTIONALITY_ANALYSIS = 600 # 10 minutes - functionality analysis
|
31
|
+
TIMEOUT_DOCUMENTATION_ANALYSIS = 300 # 5 minutes - documentation analysis
|
32
|
+
TIMEOUT_STATIC_ANALYSIS = 450 # 7.5 minutes - static analysis
|
33
|
+
TIMEOUT_REFACTORING_RECOMMENDATIONS = 600 # 10 minutes - refactoring
|
34
|
+
|
20
35
|
attr_reader :activity_state, :last_activity_time, :start_time, :step_name
|
21
36
|
|
22
37
|
def initialize(output: nil, prompt: TTY::Prompt.new)
|
@@ -48,6 +63,12 @@ module Aidp
|
|
48
63
|
raise NotImplementedError, "#{self.class} must implement #name"
|
49
64
|
end
|
50
65
|
|
66
|
+
# Human-friendly display name for UI
|
67
|
+
# Override in subclasses to provide a better display name
|
68
|
+
def display_name
|
69
|
+
name
|
70
|
+
end
|
71
|
+
|
51
72
|
def send(prompt:, session: nil)
|
52
73
|
raise NotImplementedError, "#{self.class} must implement #send"
|
53
74
|
end
|
@@ -332,20 +353,30 @@ module Aidp
|
|
332
353
|
(success_rate * 50) + ((1 - rate_limit_ratio) * 30) + (response_time_score * 0.2)
|
333
354
|
end
|
334
355
|
|
335
|
-
|
356
|
+
protected
|
336
357
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
else
|
358
|
+
# Update spinner status with elapsed time
|
359
|
+
# This is a shared method used by all providers to display progress
|
360
|
+
def update_spinner_status(spinner, elapsed, provider_name)
|
361
|
+
minutes = (elapsed / 60).to_i
|
362
|
+
seconds = (elapsed % 60).to_i
|
363
|
+
|
364
|
+
if minutes > 0
|
365
|
+
spinner.update(title: "#{provider_name} is running... (#{minutes}m #{seconds}s)")
|
366
|
+
else
|
367
|
+
spinner.update(title: "#{provider_name} is running... (#{seconds}s)")
|
346
368
|
end
|
347
|
-
@prompt.say(message, color: color)
|
348
369
|
end
|
370
|
+
|
371
|
+
# Clean up activity display thread and spinner
|
372
|
+
# Used by providers to ensure proper cleanup in both success and error paths
|
373
|
+
def cleanup_activity_display(activity_display_thread, spinner)
|
374
|
+
activity_display_thread.kill if activity_display_thread&.alive?
|
375
|
+
activity_display_thread&.join(0.1) # Give it 100ms to finish
|
376
|
+
spinner&.stop
|
377
|
+
end
|
378
|
+
|
379
|
+
private
|
349
380
|
end
|
350
381
|
end
|
351
382
|
end
|