aidp 0.3.0 → 0.7.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +191 -5
  3. data/lib/aidp/analysis/kb_inspector.rb +456 -0
  4. data/lib/aidp/analysis/seams.rb +188 -0
  5. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +493 -0
  6. data/lib/aidp/analysis/tree_sitter_scan.rb +703 -0
  7. data/lib/aidp/analyze/agent_personas.rb +1 -1
  8. data/lib/aidp/analyze/agent_tool_executor.rb +5 -11
  9. data/lib/aidp/analyze/data_retention_manager.rb +0 -5
  10. data/lib/aidp/analyze/database.rb +99 -82
  11. data/lib/aidp/analyze/error_handler.rb +12 -79
  12. data/lib/aidp/analyze/export_manager.rb +0 -7
  13. data/lib/aidp/analyze/focus_guidance.rb +2 -2
  14. data/lib/aidp/analyze/incremental_analyzer.rb +1 -11
  15. data/lib/aidp/analyze/large_analysis_progress.rb +0 -5
  16. data/lib/aidp/analyze/memory_manager.rb +34 -60
  17. data/lib/aidp/analyze/metrics_storage.rb +336 -0
  18. data/lib/aidp/analyze/parallel_processor.rb +0 -6
  19. data/lib/aidp/analyze/performance_optimizer.rb +0 -3
  20. data/lib/aidp/analyze/prioritizer.rb +2 -2
  21. data/lib/aidp/analyze/repository_chunker.rb +14 -21
  22. data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
  23. data/lib/aidp/analyze/runner.rb +107 -191
  24. data/lib/aidp/analyze/steps.rb +35 -30
  25. data/lib/aidp/analyze/storage.rb +233 -178
  26. data/lib/aidp/analyze/tool_configuration.rb +21 -36
  27. data/lib/aidp/cli/jobs_command.rb +489 -0
  28. data/lib/aidp/cli/terminal_io.rb +52 -0
  29. data/lib/aidp/cli.rb +160 -45
  30. data/lib/aidp/core_ext/class_attribute.rb +36 -0
  31. data/lib/aidp/database/pg_adapter.rb +148 -0
  32. data/lib/aidp/database_config.rb +69 -0
  33. data/lib/aidp/database_connection.rb +72 -0
  34. data/lib/aidp/execute/runner.rb +65 -92
  35. data/lib/aidp/execute/steps.rb +81 -82
  36. data/lib/aidp/job_manager.rb +41 -0
  37. data/lib/aidp/jobs/base_job.rb +45 -0
  38. data/lib/aidp/jobs/provider_execution_job.rb +83 -0
  39. data/lib/aidp/provider_manager.rb +25 -0
  40. data/lib/aidp/providers/agent_supervisor.rb +348 -0
  41. data/lib/aidp/providers/anthropic.rb +160 -3
  42. data/lib/aidp/providers/base.rb +153 -6
  43. data/lib/aidp/providers/cursor.rb +245 -43
  44. data/lib/aidp/providers/gemini.rb +164 -3
  45. data/lib/aidp/providers/supervised_base.rb +317 -0
  46. data/lib/aidp/providers/supervised_cursor.rb +22 -0
  47. data/lib/aidp/version.rb +1 -1
  48. data/lib/aidp.rb +31 -34
  49. data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
  50. data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
  51. metadata +91 -36
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+ require "open3"
5
+
6
+ module Aidp
7
+ module Providers
8
+ # Supervisor for managing agent execution with progressive warnings instead of hard timeouts
9
+ class AgentSupervisor
10
+ # Execution states
11
+ STATES = {
12
+ idle: "⏳",
13
+ starting: "🚀",
14
+ running: "🔄",
15
+ warning: "⚠️",
16
+ user_aborted: "🛑",
17
+ completed: "✅",
18
+ failed: "❌"
19
+ }.freeze
20
+
21
+ attr_reader :state, :start_time, :end_time, :duration, :output, :error_output, :exit_code
22
+
23
+ def initialize(command, timeout_seconds: 300, debug: false)
24
+ @command = command
25
+ @timeout_seconds = timeout_seconds
26
+ @debug = debug
27
+ @state = :idle
28
+ @start_time = nil
29
+ @end_time = nil
30
+ @duration = 0
31
+ @output = ""
32
+ @error_output = ""
33
+ @exit_code = nil
34
+ @process_pid = nil
35
+ @output_count = 0
36
+ @last_output_time = nil
37
+ @supervisor_thread = nil
38
+ @output_threads = []
39
+ @warning_shown = false
40
+ @user_aborted = false
41
+ end
42
+
43
+ # Execute the command with supervision
44
+ def execute(input = nil)
45
+ @state = :starting
46
+ @start_time = Time.now
47
+ @last_output_time = @start_time
48
+
49
+ puts "🚀 Starting agent execution (will warn at #{@timeout_seconds}s)"
50
+
51
+ begin
52
+ # Start the process
53
+ Open3.popen3(*@command) do |stdin, stdout, stderr, wait|
54
+ @process_pid = wait.pid
55
+ @state = :running
56
+
57
+ # Send input if provided
58
+ if input
59
+ stdin.puts input
60
+ stdin.close
61
+ end
62
+
63
+ # Start timeout thread that will warn but not kill
64
+ timeout_thread = Thread.new do
65
+ sleep @timeout_seconds
66
+ if @state == :running && !@warning_shown
67
+ show_timeout_warning
68
+ end
69
+ end
70
+
71
+ # Start supervisor thread
72
+ start_supervisor_thread(wait)
73
+
74
+ # Start output collection threads
75
+ start_output_threads(stdout, stderr)
76
+
77
+ # Wait for completion
78
+ result = wait_for_completion(wait)
79
+
80
+ # Kill timeout thread since we're done
81
+ timeout_thread.kill
82
+
83
+ # Clean up threads
84
+ cleanup_threads
85
+
86
+ @end_time = Time.now
87
+ @duration = @end_time - @start_time
88
+
89
+ return result
90
+ end
91
+ rescue => e
92
+ @state = :failed
93
+ @end_time = Time.now
94
+ @duration = @end_time - @start_time if @start_time
95
+ puts "❌ Agent execution failed: #{e.message}"
96
+ raise
97
+ end
98
+ end
99
+
100
+ # Get current execution status
101
+ def status
102
+ elapsed = @start_time ? Time.now - @start_time : 0
103
+ minutes = (elapsed / 60).to_i
104
+ seconds = (elapsed % 60).to_i
105
+ time_str = (minutes > 0) ? "#{minutes}m #{seconds}s" : "#{seconds}s"
106
+
107
+ case @state
108
+ when :idle
109
+ "⏳ Idle"
110
+ when :starting
111
+ "🚀 Starting..."
112
+ when :running
113
+ output_info = (@output_count > 0) ? " (#{@output_count} outputs)" : ""
114
+ "🔄 Running #{time_str}#{output_info}"
115
+ when :warning
116
+ "⚠️ Taking longer than expected #{time_str}"
117
+ when :user_aborted
118
+ "🛑 Aborted by user after #{time_str}"
119
+ when :completed
120
+ "✅ Completed in #{time_str}"
121
+ when :failed
122
+ "❌ Failed after #{time_str}"
123
+ end
124
+ end
125
+
126
+ # Check if execution is still active
127
+ def active?
128
+ [:starting, :running, :warning].include?(@state)
129
+ end
130
+
131
+ # Check if execution completed successfully
132
+ def success?
133
+ @state == :completed && @exit_code == 0
134
+ end
135
+
136
+ # Show timeout warning and give user control
137
+ def show_timeout_warning
138
+ return if @warning_shown
139
+ @warning_shown = true
140
+ @state = :warning
141
+
142
+ puts "\n⚠️ Agent has been running for #{@timeout_seconds} seconds"
143
+ puts " This is longer than expected, but the agent may still be working."
144
+ puts " You can:"
145
+ puts " 1. Continue waiting (press Enter)"
146
+ puts " 2. Abort execution (type 'abort' and press Enter)"
147
+ puts " 3. Wait 5 more minutes (type 'wait' and press Enter)"
148
+
149
+ begin
150
+ Timeout.timeout(30) do
151
+ response = gets&.chomp&.downcase
152
+ case response
153
+ when "abort"
154
+ puts "🛑 Aborting execution..."
155
+ @user_aborted = true
156
+ @state = :user_aborted
157
+ kill!
158
+ when "wait"
159
+ puts "⏰ Will warn again in 5 minutes..."
160
+ @warning_shown = false
161
+ @state = :running
162
+ # Start another warning thread for 5 more minutes
163
+ Thread.new do
164
+ sleep 300 # 5 minutes
165
+ if @state == :running && !@warning_shown
166
+ show_timeout_warning
167
+ end
168
+ end
169
+ else
170
+ puts "🔄 Continuing to wait..."
171
+ @state = :running
172
+ end
173
+ end
174
+ rescue Timeout::Error
175
+ puts "⏰ No response received, continuing to wait..."
176
+ @state = :running
177
+ rescue Interrupt
178
+ puts "\n🛑 User interrupted, aborting..."
179
+ @user_aborted = true
180
+ @state = :user_aborted
181
+ kill!
182
+ end
183
+ end
184
+
185
+ # Force kill the process
186
+ def kill!
187
+ return unless @process_pid && active?
188
+
189
+ puts "💀 Force killing agent process (PID: #{@process_pid})"
190
+
191
+ begin
192
+ # Try graceful termination first
193
+ Process.kill("TERM", @process_pid)
194
+ sleep 1
195
+
196
+ # Force kill if still running
197
+ if process_running?(@process_pid)
198
+ Process.kill("KILL", @process_pid)
199
+ sleep 1
200
+ end
201
+
202
+ # Double-check and force kill again if needed
203
+ if process_running?(@process_pid)
204
+ puts "⚠️ Process still running, using SIGKILL..."
205
+ Process.kill("KILL", @process_pid)
206
+ sleep 1
207
+ end
208
+
209
+ @state = :user_aborted
210
+ rescue Errno::ESRCH
211
+ # Process already dead
212
+ @state = :user_aborted
213
+ rescue => e
214
+ puts "⚠️ Error killing process: #{e.message}"
215
+ # Try one more time with KILL
216
+ begin
217
+ Process.kill("KILL", @process_pid) if process_running?(@process_pid)
218
+ rescue Errno::ESRCH
219
+ # Process already terminated
220
+ end
221
+ end
222
+ end
223
+
224
+ private
225
+
226
+ def start_supervisor_thread(wait)
227
+ @supervisor_thread = Thread.new do
228
+ loop do
229
+ sleep 10 # Check every 10 seconds
230
+
231
+ # Check if process is done
232
+ if wait.value
233
+ break
234
+ end
235
+
236
+ # Check for stuck condition (no output for 3 minutes)
237
+ if @last_output_time && Time.now - @last_output_time > 180
238
+ puts "⚠️ Agent appears stuck (no output for 3+ minutes)"
239
+ # Don't kill, just warn
240
+ end
241
+ end
242
+ rescue => e
243
+ puts "⚠️ Supervisor thread error: #{e.message}" if @debug
244
+ end
245
+
246
+ @supervisor_thread
247
+ end
248
+
249
+ def start_output_threads(stdout, stderr)
250
+ # Stdout thread
251
+ @output_threads << Thread.new do
252
+ stdout.each_line do |line|
253
+ @output += line
254
+ @output_count += 1
255
+ @last_output_time = Time.now
256
+
257
+ if @debug
258
+ puts "📤 #{line.chomp}"
259
+ end
260
+ end
261
+ rescue IOError => e
262
+ puts "📤 stdout closed: #{e.message}" if @debug
263
+ rescue => e
264
+ puts "⚠️ stdout thread error: #{e.message}" if @debug
265
+ end
266
+
267
+ # Stderr thread
268
+ @output_threads << Thread.new do
269
+ stderr.each_line do |line|
270
+ @error_output += line
271
+ @output_count += 1
272
+ @last_output_time = Time.now
273
+
274
+ if @debug
275
+ puts "❌ #{line.chomp}"
276
+ end
277
+ end
278
+ rescue IOError => e
279
+ puts "❌ stderr closed: #{e.message}" if @debug
280
+ rescue => e
281
+ puts "⚠️ stderr thread error: #{e.message}" if @debug
282
+ end
283
+ end
284
+
285
+ def wait_for_completion(wait)
286
+ # Wait for process to complete
287
+ exit_status = wait.value
288
+ @exit_code = exit_status.exitstatus
289
+
290
+ # Update duration
291
+ @duration = Time.now - @start_time
292
+
293
+ if @user_aborted || @state == :user_aborted
294
+ # Process was killed by user
295
+ {
296
+ success: false,
297
+ state: @state,
298
+ output: @output,
299
+ error_output: @error_output,
300
+ exit_code: @exit_code,
301
+ duration: @duration,
302
+ reason: "user_aborted"
303
+ }
304
+ elsif exit_status.success?
305
+ @state = :completed
306
+ {
307
+ success: true,
308
+ state: @state,
309
+ output: @output,
310
+ error_output: @error_output,
311
+ exit_code: @exit_code,
312
+ duration: @duration
313
+ }
314
+ else
315
+ @state = :failed
316
+ {
317
+ success: false,
318
+ state: @state,
319
+ output: @output,
320
+ error_output: @error_output,
321
+ exit_code: @exit_code,
322
+ duration: @duration,
323
+ reason: "non_zero_exit"
324
+ }
325
+ end
326
+ end
327
+
328
+ def cleanup_threads
329
+ # Wait for output threads to finish (with timeout)
330
+ @output_threads.each do |thread|
331
+ thread.join(5) # Wait up to 5 seconds
332
+ rescue => e
333
+ puts "⚠️ Error joining thread: #{e.message}" if @debug
334
+ end
335
+
336
+ # Kill supervisor thread
337
+ @supervisor_thread&.kill
338
+ end
339
+
340
+ def process_running?(pid)
341
+ Process.kill(0, pid)
342
+ true
343
+ rescue Errno::ESRCH
344
+ false
345
+ end
346
+ end
347
+ end
348
+ end
@@ -14,10 +14,167 @@ module Aidp
14
14
  def send(prompt:, session: nil)
15
15
  raise "claude CLI not available" unless self.class.available?
16
16
 
17
+ require "open3"
18
+
17
19
  # Use Claude CLI for non-interactive mode
18
- cmd = ["claude", "compose", "--prompt", prompt]
19
- system(*cmd)
20
- :ok
20
+ cmd = ["claude", "--print"]
21
+
22
+ puts "📝 Sending prompt to claude..."
23
+
24
+ # Smart timeout calculation
25
+ timeout_seconds = calculate_timeout
26
+
27
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait|
28
+ # Send the prompt to stdin
29
+ stdin.puts prompt
30
+ stdin.close
31
+
32
+ # Start stuck detection thread
33
+ stuck_detection_thread = Thread.new do
34
+ loop do
35
+ sleep 10 # Check every 10 seconds
36
+
37
+ if stuck?
38
+ puts "⚠️ claude appears stuck (no activity for #{stuck_timeout} seconds)"
39
+ puts " You can:"
40
+ puts " 1. Wait longer (press Enter)"
41
+ puts " 2. Abort (Ctrl+C)"
42
+
43
+ # Give user a chance to respond
44
+ begin
45
+ Timeout.timeout(30) do
46
+ gets
47
+ puts "🔄 Continuing to wait..."
48
+ end
49
+ rescue Timeout::Error
50
+ puts "⏰ No response received, continuing to wait..."
51
+ rescue Interrupt
52
+ puts "🛑 Aborting claude..."
53
+ Process.kill("TERM", wait.pid)
54
+ raise Interrupt, "User aborted claude execution"
55
+ end
56
+ end
57
+
58
+ # Stop checking if the process is done
59
+ break if wait.value
60
+ end
61
+ end
62
+
63
+ # Wait for completion with timeout
64
+ begin
65
+ Timeout.timeout(timeout_seconds) do
66
+ result = wait.value
67
+
68
+ # Stop stuck detection thread
69
+ stuck_detection_thread&.kill
70
+
71
+ if result.success?
72
+ output = stdout.read
73
+ puts "✅ Claude analysis completed"
74
+ mark_completed
75
+ return output.empty? ? :ok : output
76
+ else
77
+ error_output = stderr.read
78
+ mark_failed("claude failed with exit code #{result.exitstatus}: #{error_output}")
79
+ raise "claude failed with exit code #{result.exitstatus}: #{error_output}"
80
+ end
81
+ end
82
+ rescue Timeout::Error
83
+ # Stop stuck detection thread
84
+ stuck_detection_thread&.kill
85
+
86
+ # Kill the process if it's taking too long
87
+ begin
88
+ Process.kill("TERM", wait.pid)
89
+ rescue Errno::ESRCH
90
+ # Process already terminated
91
+ end
92
+
93
+ mark_failed("claude timed out after #{timeout_seconds} seconds")
94
+ raise Timeout::Error, "claude timed out after #{timeout_seconds} seconds"
95
+ rescue Interrupt
96
+ # Stop stuck detection thread
97
+ stuck_detection_thread&.kill
98
+
99
+ # Kill the process
100
+ begin
101
+ Process.kill("TERM", wait.pid)
102
+ rescue Errno::ESRCH
103
+ # Process already terminated
104
+ end
105
+
106
+ mark_failed("claude execution was interrupted")
107
+ raise
108
+ end
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def calculate_timeout
115
+ # Priority order for timeout calculation:
116
+ # 1. Quick mode (for testing)
117
+ # 2. Environment variable override
118
+ # 3. Adaptive timeout based on step type
119
+ # 4. Default timeout
120
+
121
+ if ENV["AIDP_QUICK_MODE"]
122
+ puts "⚡ Quick mode enabled - 2 minute timeout"
123
+ return 120
124
+ end
125
+
126
+ if ENV["AIDP_ANTHROPIC_TIMEOUT"]
127
+ return ENV["AIDP_ANTHROPIC_TIMEOUT"].to_i
128
+ end
129
+
130
+ # Adaptive timeout based on step type
131
+ step_timeout = get_adaptive_timeout
132
+ if step_timeout
133
+ puts "🧠 Using adaptive timeout: #{step_timeout} seconds"
134
+ return step_timeout
135
+ end
136
+
137
+ # Default timeout (5 minutes for interactive use)
138
+ puts "📋 Using default timeout: 5 minutes"
139
+ 300
140
+ end
141
+
142
+ def get_adaptive_timeout
143
+ # Try to get timeout recommendations from metrics storage
144
+ require_relative "../analyze/metrics_storage"
145
+ storage = Aidp::Analyze::MetricsStorage.new(Dir.pwd)
146
+ recommendations = storage.calculate_timeout_recommendations
147
+
148
+ # Get current step name from environment or context
149
+ step_name = ENV["AIDP_CURRENT_STEP"] || "unknown"
150
+
151
+ if recommendations[step_name]
152
+ recommended = recommendations[step_name][:recommended_timeout]
153
+ # Add 20% buffer for safety
154
+ return (recommended * 1.2).ceil
155
+ end
156
+
157
+ # Fallback timeouts based on step type patterns
158
+ step_name = ENV["AIDP_CURRENT_STEP"] || ""
159
+
160
+ case step_name
161
+ when /REPOSITORY_ANALYSIS/
162
+ 180 # 3 minutes - repository analysis can be quick
163
+ when /ARCHITECTURE_ANALYSIS/
164
+ 600 # 10 minutes - architecture analysis needs more time
165
+ when /TEST_ANALYSIS/
166
+ 300 # 5 minutes - test analysis is moderate
167
+ when /FUNCTIONALITY_ANALYSIS/
168
+ 600 # 10 minutes - functionality analysis is complex
169
+ when /DOCUMENTATION_ANALYSIS/
170
+ 300 # 5 minutes - documentation analysis is moderate
171
+ when /STATIC_ANALYSIS/
172
+ 450 # 7.5 minutes - static analysis can be intensive
173
+ when /REFACTORING_RECOMMENDATIONS/
174
+ 600 # 10 minutes - refactoring recommendations are complex
175
+ else
176
+ nil # Use default
177
+ end
21
178
  end
22
179
  end
23
180
  end
@@ -3,13 +3,160 @@
3
3
  module Aidp
4
4
  module Providers
5
5
  class Base
6
- def name = raise(NotImplementedError)
6
+ # Activity indicator states
7
+ ACTIVITY_STATES = {
8
+ idle: "⏳",
9
+ working: "🔄",
10
+ stuck: "⚠️",
11
+ completed: "✅",
12
+ failed: "❌"
13
+ }.freeze
7
14
 
8
- # Send a composed prompt string to the provider.
9
- # Return :ok when command completed successfully,
10
- # Return :interactive when starting an interactive session (for gate steps),
11
- # or return a string if we captured output and the caller should write to a file.
12
- def send(prompt:, session: nil) = raise(NotImplementedError)
15
+ # Default timeout for stuck detection (2 minutes)
16
+ DEFAULT_STUCK_TIMEOUT = 120
17
+
18
+ attr_reader :activity_state, :last_activity_time, :start_time, :step_name
19
+
20
+ def initialize
21
+ @activity_state = :idle
22
+ @last_activity_time = Time.now
23
+ @start_time = nil
24
+ @step_name = nil
25
+ @activity_callback = nil
26
+ @stuck_timeout = DEFAULT_STUCK_TIMEOUT
27
+ @output_count = 0
28
+ @last_output_time = Time.now
29
+ @job_context = nil
30
+ end
31
+
32
+ def name
33
+ raise NotImplementedError, "#{self.class} must implement #name"
34
+ end
35
+
36
+ def send(prompt:, session: nil)
37
+ raise NotImplementedError, "#{self.class} must implement #send"
38
+ end
39
+
40
+ # Set job context for background execution
41
+ def set_job_context(job_id:, execution_id:, job_manager:)
42
+ @job_context = {
43
+ job_id: job_id,
44
+ execution_id: execution_id,
45
+ job_manager: job_manager
46
+ }
47
+ end
48
+
49
+ # Set up activity monitoring for a step
50
+ def setup_activity_monitoring(step_name, activity_callback = nil, stuck_timeout = nil)
51
+ @step_name = step_name
52
+ @activity_callback = activity_callback
53
+ @stuck_timeout = stuck_timeout || DEFAULT_STUCK_TIMEOUT
54
+ @start_time = Time.now
55
+ @last_activity_time = @start_time
56
+ @output_count = 0
57
+ @last_output_time = @start_time
58
+ update_activity_state(:working)
59
+ end
60
+
61
+ # Update activity state and notify callback
62
+ def update_activity_state(state, message = nil)
63
+ @activity_state = state
64
+ @last_activity_time = Time.now if state == :working
65
+
66
+ # Log state change to job if in background mode
67
+ if @job_context
68
+ level = case state
69
+ when :completed then "info"
70
+ when :failed then "error"
71
+ else "debug"
72
+ end
73
+
74
+ log_to_job(message || "Provider state changed to #{state}", level)
75
+ end
76
+
77
+ @activity_callback&.call(state, message, self)
78
+ end
79
+
80
+ # Check if provider appears to be stuck
81
+ def stuck?
82
+ return false unless @activity_state == :working
83
+
84
+ time_since_activity = Time.now - @last_activity_time
85
+ time_since_activity > @stuck_timeout
86
+ end
87
+
88
+ # Get current execution time
89
+ def execution_time
90
+ return 0 unless @start_time
91
+ Time.now - @start_time
92
+ end
93
+
94
+ # Get time since last activity
95
+ def time_since_last_activity
96
+ Time.now - @last_activity_time
97
+ end
98
+
99
+ # Record activity (called when provider produces output)
100
+ def record_activity(message = nil)
101
+ @output_count += 1
102
+ @last_output_time = Time.now
103
+ update_activity_state(:working, message)
104
+ end
105
+
106
+ # Mark as completed
107
+ def mark_completed
108
+ update_activity_state(:completed)
109
+ end
110
+
111
+ # Mark as failed
112
+ def mark_failed(error_message = nil)
113
+ update_activity_state(:failed, error_message)
114
+ end
115
+
116
+ # Get activity summary for metrics
117
+ def activity_summary
118
+ {
119
+ provider: name,
120
+ step_name: @step_name,
121
+ start_time: @start_time&.iso8601,
122
+ end_time: Time.now.iso8601,
123
+ duration: execution_time,
124
+ final_state: @activity_state,
125
+ stuck_detected: stuck?,
126
+ output_count: @output_count
127
+ }
128
+ end
129
+
130
+ # Check if provider supports activity monitoring
131
+ def supports_activity_monitoring?
132
+ true # Default to true, override in subclasses if needed
133
+ end
134
+
135
+ # Get stuck timeout for this provider
136
+ attr_reader :stuck_timeout
137
+
138
+ protected
139
+
140
+ # Log message to job if in background mode
141
+ def log_to_job(message, level = "info", metadata = {})
142
+ return unless @job_context && @job_context[:job_manager]
143
+
144
+ metadata = metadata.merge(
145
+ provider: name,
146
+ step_name: @step_name,
147
+ activity_state: @activity_state,
148
+ execution_time: execution_time,
149
+ output_count: @output_count
150
+ )
151
+
152
+ @job_context[:job_manager].log_message(
153
+ @job_context[:job_id],
154
+ @job_context[:execution_id],
155
+ message,
156
+ level,
157
+ metadata
158
+ )
159
+ end
13
160
  end
14
161
  end
15
162
  end