aidp 0.9.6 → 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.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +194 -25
  3. data/lib/aidp/analyze/error_handler.rb +4 -2
  4. data/lib/aidp/{analysis → analyze}/kb_inspector.rb +93 -89
  5. data/lib/aidp/analyze/prioritizer.rb +3 -2
  6. data/lib/aidp/analyze/progress.rb +2 -1
  7. data/lib/aidp/analyze/ruby_maat_integration.rb +7 -3
  8. data/lib/aidp/analyze/runner.rb +73 -11
  9. data/lib/aidp/{analysis → analyze}/seams.rb +1 -1
  10. data/lib/aidp/analyze/steps.rb +10 -8
  11. data/lib/aidp/{analysis → analyze}/tree_sitter_grammar_loader.rb +11 -5
  12. data/lib/aidp/{analysis → analyze}/tree_sitter_scan.rb +21 -15
  13. data/lib/aidp/cli/checkpoint_command.rb +98 -0
  14. data/lib/aidp/cli/first_run_wizard.rb +83 -103
  15. data/lib/aidp/cli/jobs_command.rb +270 -36
  16. data/lib/aidp/cli/terminal_io.rb +3 -3
  17. data/lib/aidp/cli.rb +411 -69
  18. data/lib/aidp/config.rb +5 -8
  19. data/lib/aidp/debug_logger.rb +4 -4
  20. data/lib/aidp/debug_mixin.rb +11 -4
  21. data/lib/aidp/execute/checkpoint.rb +282 -0
  22. data/lib/aidp/execute/checkpoint_display.rb +221 -0
  23. data/lib/aidp/execute/progress.rb +2 -1
  24. data/lib/aidp/execute/prompt_manager.rb +62 -0
  25. data/lib/aidp/execute/runner.rb +67 -20
  26. data/lib/aidp/execute/steps.rb +36 -27
  27. data/lib/aidp/execute/work_loop_runner.rb +308 -0
  28. data/lib/aidp/execute/workflow_selector.rb +50 -26
  29. data/lib/aidp/harness/condition_detector.rb +4 -4
  30. data/lib/aidp/harness/config_schema.rb +40 -0
  31. data/lib/aidp/harness/config_validator.rb +3 -6
  32. data/lib/aidp/harness/configuration.rb +35 -1
  33. data/lib/aidp/harness/enhanced_runner.rb +25 -4
  34. data/lib/aidp/harness/error_handler.rb +103 -28
  35. data/lib/aidp/harness/provider_factory.rb +6 -1
  36. data/lib/aidp/harness/provider_manager.rb +273 -19
  37. data/lib/aidp/harness/runner.rb +14 -6
  38. data/lib/aidp/harness/simple_user_interface.rb +6 -4
  39. data/lib/aidp/harness/status_display.rb +118 -106
  40. data/lib/aidp/harness/test_runner.rb +83 -0
  41. data/lib/aidp/harness/ui/enhanced_tui.rb +7 -5
  42. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +22 -4
  43. data/lib/aidp/harness/ui/error_handler.rb +7 -2
  44. data/lib/aidp/harness/ui/frame_manager.rb +61 -39
  45. data/lib/aidp/harness/ui/job_monitor.rb +2 -0
  46. data/lib/aidp/harness/ui/navigation/main_menu.rb +27 -16
  47. data/lib/aidp/harness/ui/navigation/menu_item.rb +1 -0
  48. data/lib/aidp/harness/ui/navigation/menu_state.rb +1 -0
  49. data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
  50. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +2 -0
  51. data/lib/aidp/harness/ui/progress_display.rb +26 -7
  52. data/lib/aidp/harness/ui/question_collector.rb +2 -0
  53. data/lib/aidp/harness/ui/spinner_group.rb +2 -0
  54. data/lib/aidp/harness/ui/spinner_helper.rb +1 -1
  55. data/lib/aidp/harness/ui/status_manager.rb +4 -2
  56. data/lib/aidp/harness/ui/status_widget.rb +20 -9
  57. data/lib/aidp/harness/ui/workflow_controller.rb +27 -9
  58. data/lib/aidp/harness/user_interface.rb +338 -330
  59. data/lib/aidp/jobs/background_runner.rb +278 -0
  60. data/lib/aidp/message_display.rb +48 -0
  61. data/lib/aidp/provider_manager.rb +13 -7
  62. data/lib/aidp/providers/anthropic.rb +101 -18
  63. data/lib/aidp/providers/base.rb +51 -1
  64. data/lib/aidp/providers/codex.rb +248 -0
  65. data/lib/aidp/providers/cursor.rb +39 -48
  66. data/lib/aidp/providers/gemini.rb +26 -16
  67. data/lib/aidp/providers/github_copilot.rb +263 -0
  68. data/lib/aidp/providers/opencode.rb +38 -47
  69. data/lib/aidp/version.rb +1 -1
  70. data/lib/aidp/workflows/definitions.rb +357 -0
  71. data/lib/aidp/workflows/selector.rb +171 -0
  72. data/lib/aidp.rb +16 -4
  73. data/templates/planning/generate_llm_style_guide.md +119 -0
  74. metadata +43 -31
  75. data/lib/aidp/analyze/progress_visualizer.rb +0 -314
  76. /data/templates/{ANALYZE/02_ARCHITECTURE_ANALYSIS.md → analysis/analyze_architecture.md} +0 -0
  77. /data/templates/{ANALYZE/05_DOCUMENTATION_ANALYSIS.md → analysis/analyze_documentation.md} +0 -0
  78. /data/templates/{ANALYZE/04_FUNCTIONALITY_ANALYSIS.md → analysis/analyze_functionality.md} +0 -0
  79. /data/templates/{ANALYZE/01_REPOSITORY_ANALYSIS.md → analysis/analyze_repository.md} +0 -0
  80. /data/templates/{ANALYZE/06_STATIC_ANALYSIS.md → analysis/analyze_static_code.md} +0 -0
  81. /data/templates/{ANALYZE/03_TEST_ANALYSIS.md → analysis/analyze_tests.md} +0 -0
  82. /data/templates/{ANALYZE/07_REFACTORING_RECOMMENDATIONS.md → analysis/recommend_refactoring.md} +0 -0
  83. /data/templates/{ANALYZE/06a_tree_sitter_scan.md → analysis/scan_with_tree_sitter.md} +0 -0
  84. /data/templates/{EXECUTE/11_STATIC_ANALYSIS.md → implementation/configure_static_analysis.md} +0 -0
  85. /data/templates/{EXECUTE/14_DOCS_PORTAL.md → implementation/create_documentation_portal.md} +0 -0
  86. /data/templates/{EXECUTE/10_IMPLEMENTATION_AGENT.md → implementation/implement_features.md} +0 -0
  87. /data/templates/{EXECUTE/13_DELIVERY_ROLLOUT.md → implementation/plan_delivery.md} +0 -0
  88. /data/templates/{EXECUTE/15_POST_RELEASE.md → implementation/review_post_release.md} +0 -0
  89. /data/templates/{EXECUTE/09_SCAFFOLDING_DEVEX.md → implementation/setup_scaffolding.md} +0 -0
  90. /data/templates/{EXECUTE/02A_ARCH_GATE_QUESTIONS.md → planning/ask_architecture_questions.md} +0 -0
  91. /data/templates/{EXECUTE/00_PRD.md → planning/create_prd.md} +0 -0
  92. /data/templates/{EXECUTE/08_TASKS.md → planning/create_tasks.md} +0 -0
  93. /data/templates/{EXECUTE/04_DOMAIN_DECOMPOSITION.md → planning/decompose_domain.md} +0 -0
  94. /data/templates/{EXECUTE/01_NFRS.md → planning/define_nfrs.md} +0 -0
  95. /data/templates/{EXECUTE/05_CONTRACTS.md → planning/design_apis.md} +0 -0
  96. /data/templates/{EXECUTE/02_ARCHITECTURE.md → planning/design_architecture.md} +0 -0
  97. /data/templates/{EXECUTE/06_THREAT_MODEL.md → planning/design_data_model.md} +0 -0
  98. /data/templates/{EXECUTE/03_ADR_FACTORY.md → planning/generate_adrs.md} +0 -0
  99. /data/templates/{EXECUTE/12_OBSERVABILITY_SLOS.md → planning/plan_observability.md} +0 -0
  100. /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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "tty-prompt"
3
4
  require_relative "harness/provider_factory"
4
5
 
5
6
  module Aidp
@@ -13,7 +14,8 @@ module Aidp
13
14
  end
14
15
 
15
16
  # Fallback to legacy method
16
- create_legacy_provider(provider_type)
17
+ prompt = options[:prompt] || TTY::Prompt.new
18
+ create_legacy_provider(provider_type, prompt: prompt)
17
19
  end
18
20
 
19
21
  def load_from_config(config = {}, options = {})
@@ -132,16 +134,20 @@ module Aidp
132
134
 
133
135
  private
134
136
 
135
- def create_legacy_provider(provider_type)
137
+ def create_legacy_provider(provider_type, prompt: TTY::Prompt.new)
136
138
  case provider_type
137
139
  when "cursor"
138
- Aidp::Providers::Cursor.new
139
- when "anthropic"
140
- Aidp::Providers::Anthropic.new
140
+ Aidp::Providers::Cursor.new(prompt: prompt)
141
+ when "anthropic", "claude"
142
+ Aidp::Providers::Anthropic.new(prompt: prompt)
141
143
  when "gemini"
142
- Aidp::Providers::Gemini.new
144
+ Aidp::Providers::Gemini.new(prompt: prompt)
143
145
  when "macos_ui"
144
- Aidp::Providers::MacOSUI.new
146
+ Aidp::Providers::MacOSUI.new(prompt: prompt)
147
+ when "github_copilot"
148
+ Aidp::Providers::GithubCopilot.new(prompt: prompt)
149
+ when "codex"
150
+ Aidp::Providers::Codex.new(prompt: prompt)
145
151
  end
146
152
  end
147
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 for better debugging
34
- result = debug_execute_command("claude", args: ["--print"], input: prompt, timeout: timeout_seconds)
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: ["--print"], input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
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
- result.out
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
- puts "⚡ Quick mode enabled - 2 minute timeout"
62
- return 120
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"]
@@ -69,13 +104,13 @@ module Aidp
69
104
  # Adaptive timeout based on step type
70
105
  step_timeout = get_adaptive_timeout
71
106
  if step_timeout
72
- puts "🧠 Using adaptive timeout: #{step_timeout} seconds"
107
+ display_message("🧠 Using adaptive timeout: #{step_timeout} seconds", type: :info)
73
108
  return step_timeout
74
109
  end
75
110
 
76
- # Default timeout (5 minutes for interactive use)
77
- puts "📋 Using default timeout: 5 minutes"
78
- 300
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
- 180 # 3 minutes - repository analysis can be quick
122
+ TIMEOUT_REPOSITORY_ANALYSIS
88
123
  when /ARCHITECTURE_ANALYSIS/
89
- 600 # 10 minutes - architecture analysis needs more time
124
+ TIMEOUT_ARCHITECTURE_ANALYSIS
90
125
  when /TEST_ANALYSIS/
91
- 300 # 5 minutes - test analysis is moderate
126
+ TIMEOUT_TEST_ANALYSIS
92
127
  when /FUNCTIONALITY_ANALYSIS/
93
- 600 # 10 minutes - functionality analysis is complex
128
+ TIMEOUT_FUNCTIONALITY_ANALYSIS
94
129
  when /DOCUMENTATION_ANALYSIS/
95
- 300 # 5 minutes - documentation analysis is moderate
130
+ TIMEOUT_DOCUMENTATION_ANALYSIS
96
131
  when /STATIC_ANALYSIS/
97
- 450 # 7.5 minutes - static analysis can be intensive
132
+ TIMEOUT_STATIC_ANALYSIS
98
133
  when /REFACTORING_RECOMMENDATIONS/
99
- 600 # 10 minutes - refactoring recommendations are complex
134
+ TIMEOUT_REFACTORING_RECOMMENDATIONS
100
135
  else
101
- nil # Use default
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
@@ -1,8 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "tty-prompt"
4
+ require "tty-spinner"
5
+
3
6
  module Aidp
4
7
  module Providers
5
8
  class Base
9
+ include Aidp::MessageDisplay
10
+
6
11
  # Activity indicator states
7
12
  ACTIVITY_STATES = {
8
13
  idle: "⏳",
@@ -15,9 +20,21 @@ module Aidp
15
20
  # Default timeout for stuck detection (2 minutes)
16
21
  DEFAULT_STUCK_TIMEOUT = 120
17
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
+
18
35
  attr_reader :activity_state, :last_activity_time, :start_time, :step_name
19
36
 
20
- def initialize
37
+ def initialize(output: nil, prompt: TTY::Prompt.new)
21
38
  @activity_state = :idle
22
39
  @last_activity_time = Time.now
23
40
  @start_time = nil
@@ -28,6 +45,8 @@ module Aidp
28
45
  @last_output_time = Time.now
29
46
  @job_context = nil
30
47
  @harness_context = nil
48
+ @output = output
49
+ @prompt = prompt
31
50
  @harness_metrics = {
32
51
  total_requests: 0,
33
52
  successful_requests: 0,
@@ -44,6 +63,12 @@ module Aidp
44
63
  raise NotImplementedError, "#{self.class} must implement #name"
45
64
  end
46
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
+
47
72
  def send(prompt:, session: nil)
48
73
  raise NotImplementedError, "#{self.class} must implement #send"
49
74
  end
@@ -327,6 +352,31 @@ module Aidp
327
352
  # Weighted health score
328
353
  (success_rate * 50) + ((1 - rate_limit_ratio) * 30) + (response_time_score * 0.2)
329
354
  end
355
+
356
+ protected
357
+
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)")
368
+ end
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
330
380
  end
331
381
  end
332
382
  end