aidp 0.15.2 → 0.16.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +47 -0
  3. data/lib/aidp/analyze/error_handler.rb +14 -15
  4. data/lib/aidp/analyze/runner.rb +27 -5
  5. data/lib/aidp/analyze/steps.rb +4 -0
  6. data/lib/aidp/cli/jobs_command.rb +2 -1
  7. data/lib/aidp/cli.rb +812 -3
  8. data/lib/aidp/concurrency/backoff.rb +148 -0
  9. data/lib/aidp/concurrency/exec.rb +192 -0
  10. data/lib/aidp/concurrency/wait.rb +148 -0
  11. data/lib/aidp/concurrency.rb +71 -0
  12. data/lib/aidp/config.rb +20 -0
  13. data/lib/aidp/daemon/runner.rb +9 -8
  14. data/lib/aidp/debug_mixin.rb +1 -0
  15. data/lib/aidp/errors.rb +12 -0
  16. data/lib/aidp/execute/interactive_repl.rb +102 -11
  17. data/lib/aidp/execute/repl_macros.rb +776 -2
  18. data/lib/aidp/execute/runner.rb +27 -5
  19. data/lib/aidp/execute/steps.rb +2 -0
  20. data/lib/aidp/harness/config_loader.rb +24 -2
  21. data/lib/aidp/harness/enhanced_runner.rb +16 -2
  22. data/lib/aidp/harness/error_handler.rb +1 -1
  23. data/lib/aidp/harness/provider_info.rb +19 -15
  24. data/lib/aidp/harness/provider_manager.rb +47 -41
  25. data/lib/aidp/harness/runner.rb +3 -11
  26. data/lib/aidp/harness/state/persistence.rb +1 -6
  27. data/lib/aidp/harness/state_manager.rb +115 -7
  28. data/lib/aidp/harness/status_display.rb +11 -18
  29. data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
  30. data/lib/aidp/harness/ui/workflow_controller.rb +1 -1
  31. data/lib/aidp/harness/user_interface.rb +12 -15
  32. data/lib/aidp/jobs/background_runner.rb +15 -5
  33. data/lib/aidp/providers/codex.rb +0 -1
  34. data/lib/aidp/providers/cursor.rb +0 -1
  35. data/lib/aidp/providers/github_copilot.rb +0 -1
  36. data/lib/aidp/providers/opencode.rb +0 -1
  37. data/lib/aidp/skills/composer.rb +178 -0
  38. data/lib/aidp/skills/loader.rb +205 -0
  39. data/lib/aidp/skills/registry.rb +220 -0
  40. data/lib/aidp/skills/skill.rb +174 -0
  41. data/lib/aidp/skills.rb +30 -0
  42. data/lib/aidp/version.rb +1 -1
  43. data/lib/aidp/watch/build_processor.rb +93 -28
  44. data/lib/aidp/watch/runner.rb +3 -2
  45. data/lib/aidp/workstream_executor.rb +244 -0
  46. data/lib/aidp/workstream_state.rb +212 -0
  47. data/lib/aidp/worktree.rb +208 -0
  48. data/lib/aidp.rb +6 -0
  49. metadata +17 -4
@@ -5,6 +5,7 @@ require_relative "steps"
5
5
  require_relative "progress"
6
6
  require_relative "work_loop_runner"
7
7
  require_relative "../storage/file_manager"
8
+ require_relative "../skills"
8
9
 
9
10
  module Aidp
10
11
  module Execute
@@ -17,6 +18,8 @@ module Aidp
17
18
  @is_harness_mode = !harness_runner.nil?
18
19
  @file_manager = Aidp::Storage::FileManager.new(File.join(project_dir, ".aidp"))
19
20
  @prompt = prompt
21
+ @skills_registry = nil # Lazy-loaded
22
+ @skills_composer = Aidp::Skills::Composer.new
20
23
  end
21
24
 
22
25
  def progress
@@ -318,18 +321,37 @@ module Aidp
318
321
  step_spec = Aidp::Execute::Steps::SPEC[step_name]
319
322
  raise "Step '#{step_name}' not found" unless step_spec
320
323
 
324
+ # Load template
321
325
  template_name = step_spec["templates"].first
322
326
  template_path = find_template(template_name)
323
327
  raise "Template not found for step #{step_name}" unless template_path
324
-
325
328
  template = File.read(template_path)
326
329
 
327
- # Replace template variables in the format {{key}} with option values
328
- options.each do |key, value|
329
- template = template.gsub("{{#{key}}}", value.to_s)
330
+ # Load skill if specified
331
+ skill = nil
332
+ if step_spec["skill"]
333
+ skill = skills_registry.find(step_spec["skill"])
334
+ if skill.nil?
335
+ Aidp.log_warn(
336
+ "skills",
337
+ "Skill not found for step",
338
+ step: step_name,
339
+ skill_id: step_spec["skill"]
340
+ )
341
+ end
330
342
  end
331
343
 
332
- template
344
+ # Compose skill + template
345
+ @skills_composer.compose(skill: skill, template: template, options: options)
346
+ end
347
+
348
+ def skills_registry
349
+ @skills_registry ||= begin
350
+ provider = @is_harness_mode ? @harness_runner.current_provider : nil
351
+ registry = Aidp::Skills::Registry.new(project_dir: @project_dir, provider: provider)
352
+ registry.load_skills
353
+ registry
354
+ end
333
355
  end
334
356
  end
335
357
  end
@@ -6,6 +6,7 @@ module Aidp
6
6
  # Simplified step specifications with fewer gates
7
7
  # Templates are now organized by purpose (planning/, analysis/, implementation/)
8
8
  # and named with action verbs for clarity
9
+ # Skills define WHO the agent is, templates define WHAT task to do
9
10
  SPEC = {
10
11
  "00_LLM_STYLE_GUIDE" => {
11
12
  "templates" => ["planning/generate_llm_style_guide.md"],
@@ -15,6 +16,7 @@ module Aidp
15
16
  "interactive" => false
16
17
  },
17
18
  "00_PRD" => {
19
+ "skill" => "product_strategist",
18
20
  "templates" => ["planning/create_prd.md"],
19
21
  "description" => "Generate Product Requirements Document",
20
22
  "outs" => ["docs/prd.md"],
@@ -3,6 +3,7 @@
3
3
  require "yaml"
4
4
  require_relative "config_schema"
5
5
  require_relative "config_validator"
6
+ require "digest"
6
7
 
7
8
  module Aidp
8
9
  module Harness
@@ -13,6 +14,7 @@ module Aidp
13
14
  @validator = ConfigValidator.new(project_dir)
14
15
  @config_cache = nil
15
16
  @last_loaded = nil
17
+ @last_signature = nil # stores {mtime:, size:, hash:}
16
18
  end
17
19
 
18
20
  # Load and validate configuration with caching
@@ -25,6 +27,7 @@ module Aidp
25
27
  if validation_result[:valid]
26
28
  @config_cache = @validator.validated_config
27
29
  @last_loaded = Time.now
30
+ @last_signature = current_file_signature
28
31
 
29
32
  # Log warnings if any
30
33
  unless validation_result[:warnings].empty?
@@ -284,13 +287,32 @@ module Aidp
284
287
  private
285
288
 
286
289
  def config_file_changed?
287
- return true unless @last_loaded && @validator.config_file_path
290
+ return true unless @last_signature && @validator.config_file_path && File.exist?(@validator.config_file_path)
288
291
 
289
- File.mtime(@validator.config_file_path) > @last_loaded
292
+ sig = current_file_signature
293
+ return true unless sig
294
+
295
+ # Detect any difference (mtime OR size OR content hash)
296
+ sig[:mtime] != @last_signature[:mtime] ||
297
+ sig[:size] != @last_signature[:size] ||
298
+ sig[:hash] != @last_signature[:hash]
290
299
  rescue
291
300
  true
292
301
  end
293
302
 
303
+ def current_file_signature
304
+ path = @validator.config_file_path
305
+ return nil unless path && File.exist?(path)
306
+ stat = File.stat(path)
307
+ {
308
+ mtime: stat.mtime,
309
+ size: stat.size,
310
+ hash: Digest::SHA256.file(path).hexdigest
311
+ }
312
+ rescue
313
+ nil
314
+ end
315
+
294
316
  def handle_validation_errors(errors)
295
317
  error_message = "Configuration validation failed:\n" + errors.join("\n")
296
318
 
@@ -190,7 +190,20 @@ module Aidp
190
190
  result = show_step_spinner(spinner_message) do
191
191
  @error_handler.execute_with_retry do
192
192
  step_options = @options.merge(user_input: @user_input)
193
- runner.run_step(step_name, step_options)
193
+ # Determine execution directory (workstream path if set)
194
+ exec_dir = begin
195
+ if @state_manager.respond_to?(:current_workstream_path)
196
+ @state_manager.current_workstream_path
197
+ else
198
+ @project_dir
199
+ end
200
+ rescue
201
+ @project_dir
202
+ end
203
+ # Execute step within the chosen directory for proper isolation
204
+ Dir.chdir(exec_dir) do
205
+ runner.run_step(step_name, step_options)
206
+ end
194
207
  end
195
208
  end
196
209
  duration = Time.now - start_time
@@ -225,8 +238,9 @@ module Aidp
225
238
  end
226
239
 
227
240
  # Remove job after a delay to show completion
241
+ # UI delay to let user see completion status before removal
228
242
  Thread.new do
229
- sleep 2
243
+ sleep 2 # Acceptable for UI timing
230
244
  @tui.remove_job(step_job_id)
231
245
  end
232
246
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "net/http"
4
4
  require_relative "../debug_mixin"
5
+ require_relative "../concurrency"
5
6
 
6
7
  module Aidp
7
8
  module Harness
@@ -191,7 +192,6 @@ module Aidp
191
192
 
192
193
  # Wait for backoff delay
193
194
  if delay > 0
194
- # Use regular sleep for now (async not needed in this context)
195
195
  sleep(delay)
196
196
  end
197
197
 
@@ -4,6 +4,7 @@ require "json"
4
4
  require "yaml"
5
5
  require "fileutils"
6
6
  require_relative "../rescue_logging"
7
+ require_relative "../concurrency"
7
8
 
8
9
  module Aidp
9
10
  module Harness
@@ -175,25 +176,28 @@ module Aidp
175
176
  pid = Process.spawn(binary_name, *args, out: w, err: w)
176
177
  w.close
177
178
 
178
- # Wait with timeout
179
- deadline = Time.now + 5
180
- status = nil
181
- while Time.now < deadline
182
- pid_done, status = Process.waitpid2(pid, Process::WNOHANG)
183
- break if pid_done
184
- sleep 0.05
185
- end
186
-
187
- # Kill if timed out
188
- unless status
179
+ begin
180
+ Aidp::Concurrency::Wait.for_process_exit(pid, timeout: 5, interval: 0.05)
181
+ # Process exited normally (status available for future diagnostics)
182
+ rescue Aidp::Concurrency::TimeoutError
183
+ # Timeout - kill process
189
184
  begin
190
185
  Process.kill("TERM", pid)
191
- sleep 0.1
192
- Process.kill("KILL", pid)
193
186
  rescue => e
194
- log_rescue(e, component: "provider_info", action: "kill_timeout_provider_command", fallback: nil, provider: @provider_name, binary: binary_name, pid: pid)
195
- nil
187
+ log_rescue(e, component: "provider_info", action: "kill_timeout_provider_command_term", fallback: nil, provider: @provider_name, binary: binary_name, pid: pid)
188
+ end
189
+
190
+ begin
191
+ Aidp::Concurrency::Wait.for_process_exit(pid, timeout: 0.1, interval: 0.02)
192
+ rescue Aidp::Concurrency::TimeoutError
193
+ # TERM didn't work, use KILL
194
+ begin
195
+ Process.kill("KILL", pid)
196
+ rescue => e
197
+ log_rescue(e, component: "provider_info", action: "kill_timeout_provider_command_kill", fallback: nil, provider: @provider_name, binary: binary_name, pid: pid)
198
+ end
196
199
  end
200
+
197
201
  return nil
198
202
  end
199
203
 
@@ -3,6 +3,7 @@
3
3
  require "tty-prompt"
4
4
  require_relative "provider_factory"
5
5
  require_relative "../rescue_logging"
6
+ require_relative "../concurrency"
6
7
 
7
8
  module Aidp
8
9
  module Harness
@@ -145,26 +146,27 @@ module Aidp
145
146
 
146
147
  # Switch provider with retry logic
147
148
  def switch_provider_with_retry(reason = "retry", max_retries = @max_retries)
148
- retry_count = 0
149
-
150
- while retry_count < max_retries
151
- next_provider = switch_provider(reason, {retry_count: retry_count})
149
+ attempt_count = 0
150
+
151
+ Aidp::Concurrency::Backoff.retry(
152
+ max_attempts: max_retries,
153
+ base: 0.5,
154
+ strategy: :exponential,
155
+ jitter: 0.2,
156
+ on: [StandardError]
157
+ ) do
158
+ attempt_count += 1
159
+ next_provider = switch_provider(reason, {retry_count: attempt_count - 1})
152
160
 
153
161
  if next_provider
154
162
  return next_provider
155
- end
156
-
157
- retry_count += 1
158
-
159
- # Wait before retrying
160
- delay = calculate_retry_delay(retry_count)
161
- if ENV["RACK_ENV"] == "test" || defined?(RSpec)
162
- sleep(delay)
163
163
  else
164
- Async::Task.current.sleep(delay)
164
+ # Raise to trigger retry
165
+ raise "Provider switch failed on attempt #{attempt_count}"
165
166
  end
166
167
  end
167
-
168
+ rescue Aidp::Concurrency::MaxAttemptsError
169
+ # All retries exhausted
168
170
  nil
169
171
  end
170
172
 
@@ -235,26 +237,27 @@ module Aidp
235
237
  def switch_model_with_retry(reason = "retry", max_retries = @max_retries)
236
238
  return nil unless @model_switching_enabled
237
239
 
238
- retry_count = 0
240
+ attempt_count = 0
239
241
 
240
- while retry_count < max_retries
241
- next_model = switch_model(reason, {retry_count: retry_count})
242
+ Aidp::Concurrency::Backoff.retry(
243
+ max_attempts: max_retries,
244
+ base: 0.5,
245
+ strategy: :exponential,
246
+ jitter: 0.2,
247
+ on: [StandardError]
248
+ ) do
249
+ attempt_count += 1
250
+ next_model = switch_model(reason, {retry_count: attempt_count - 1})
242
251
 
243
252
  if next_model
244
253
  return next_model
245
- end
246
-
247
- retry_count += 1
248
-
249
- # Wait before retrying
250
- delay = calculate_retry_delay(retry_count)
251
- if ENV["RACK_ENV"] == "test" || defined?(RSpec)
252
- sleep(delay)
253
254
  else
254
- Async::Task.current.sleep(delay)
255
+ # Raise to trigger retry
256
+ raise "Model switch failed on attempt #{attempt_count}"
255
257
  end
256
258
  end
257
-
259
+ rescue Aidp::Concurrency::MaxAttemptsError
260
+ # All retries exhausted
258
261
  nil
259
262
  end
260
263
 
@@ -1112,28 +1115,31 @@ module Aidp
1112
1115
  r, w = IO.pipe
1113
1116
  pid = Process.spawn(binary, "--version", out: w, err: w)
1114
1117
  w.close
1115
- deadline = Time.now + 3
1116
- status = nil
1117
- while Time.now < deadline
1118
- pid_done, status = Process.waitpid2(pid, Process::WNOHANG)
1119
- break if pid_done
1120
- sleep 0.05
1121
- end
1122
- unless status
1123
- # Timeout -> kill
1118
+ # Wait for process to exit with timeout
1119
+ begin
1120
+ Aidp::Concurrency::Wait.for_process_exit(pid, timeout: 3, interval: 0.05)
1121
+ rescue Aidp::Concurrency::TimeoutError
1122
+ # Timeout -> kill process
1124
1123
  begin
1125
1124
  Process.kill("TERM", pid)
1126
1125
  rescue => e
1127
1126
  log_rescue(e, component: "provider_manager", action: "kill_timeout_process_term", fallback: nil, binary: binary, pid: pid)
1128
1127
  nil
1129
1128
  end
1130
- sleep 0.1
1129
+
1130
+ # Brief wait for TERM to take effect
1131
1131
  begin
1132
- Process.kill("KILL", pid)
1133
- rescue => e
1134
- log_rescue(e, component: "provider_manager", action: "kill_timeout_process_kill", fallback: nil, binary: binary, pid: pid)
1135
- nil
1132
+ Aidp::Concurrency::Wait.for_process_exit(pid, timeout: 0.1, interval: 0.02)
1133
+ rescue Aidp::Concurrency::TimeoutError
1134
+ # TERM didn't work, use KILL
1135
+ begin
1136
+ Process.kill("KILL", pid)
1137
+ rescue => e
1138
+ log_rescue(e, component: "provider_manager", action: "kill_timeout_process_kill", fallback: nil, binary: binary, pid: pid)
1139
+ nil
1140
+ end
1136
1141
  end
1142
+
1137
1143
  ok = false
1138
1144
  reason = "binary_timeout"
1139
1145
  end
@@ -10,6 +10,7 @@ require_relative "simple_user_interface"
10
10
  require_relative "error_handler"
11
11
  require_relative "status_display"
12
12
  require_relative "completion_checker"
13
+ require_relative "../concurrency"
13
14
 
14
15
  module Aidp
15
16
  module Harness
@@ -296,11 +297,7 @@ module Aidp
296
297
  while Time.now < reset_time && @state == STATES[:waiting_for_rate_limit]
297
298
  remaining = reset_time - Time.now
298
299
  @status_display.update_rate_limit_countdown(remaining)
299
- if ENV["RACK_ENV"] == "test" || defined?(RSpec)
300
- sleep(1)
301
- else
302
- Async::Task.current.sleep(1)
303
- end
300
+ sleep(1)
304
301
  end
305
302
  end
306
303
 
@@ -319,12 +316,7 @@ module Aidp
319
316
  def handle_pause_condition
320
317
  case @state
321
318
  when STATES[:paused]
322
- # Wait for user to resume
323
- if ENV["RACK_ENV"] == "test" || defined?(RSpec)
324
- sleep(1)
325
- else
326
- Async::Task.current.sleep(1)
327
- end
319
+ sleep(1)
328
320
  when STATES[:waiting_for_user]
329
321
  # User interface handles this
330
322
  nil
@@ -107,12 +107,7 @@ module Aidp
107
107
  end
108
108
 
109
109
  def sleep_briefly
110
- require "async"
111
- if Async::Task.current?
112
- Async::Task.current.sleep(0.1)
113
- else
114
- sleep(0.1)
115
- end
110
+ sleep(0.1)
116
111
  end
117
112
 
118
113
  def raise_lock_timeout_error
@@ -216,6 +216,21 @@ module Aidp
216
216
 
217
217
  # Export state for debugging
218
218
  def export_state
219
+ # In test mode, include test variables
220
+ if ENV["RACK_ENV"] == "test" || defined?(RSpec)
221
+ test_state = {
222
+ current_workstream: @test_workstream,
223
+ workstream_path: @test_workstream_path,
224
+ workstream_branch: @test_workstream_branch
225
+ }
226
+ return {
227
+ state_file: @state_file,
228
+ has_state: false,
229
+ metadata: {},
230
+ state: test_state
231
+ }
232
+ end
233
+
219
234
  {
220
235
  state_file: @state_file,
221
236
  has_state: has_state?,
@@ -249,6 +264,15 @@ module Aidp
249
264
  @progress_tracker.mark_step_completed(step_name)
250
265
  # Also update harness state
251
266
  update_state(current_step: nil, last_step_completed: step_name)
267
+ # Increment iteration counter for current workstream if present
268
+ ws_slug = current_workstream
269
+ if ws_slug
270
+ # File layout: this file is in lib/aidp/harness/state_manager.rb
271
+ # workstream_state.rb lives at lib/aidp/workstream_state.rb
272
+ # Correct relative path from here is ../workstream_state
273
+ require_relative "../workstream_state"
274
+ Aidp::WorkstreamState.increment_iteration(slug: ws_slug, project_dir: @project_dir)
275
+ end
252
276
  end
253
277
 
254
278
  # Mark step as in progress
@@ -284,6 +308,12 @@ module Aidp
284
308
  def reset_all
285
309
  @progress_tracker.reset
286
310
  clear_state
311
+ # Also clear test workstream variables
312
+ if ENV["RACK_ENV"] == "test" || defined?(RSpec)
313
+ @test_workstream = nil
314
+ @test_workstream_path = nil
315
+ @test_workstream_branch = nil
316
+ end
287
317
  end
288
318
 
289
319
  # Get progress summary
@@ -299,7 +329,8 @@ module Aidp
299
329
  harness_state: has_state? ? load_state : {},
300
330
  progress_percentage: progress_percentage,
301
331
  session_duration: session_duration,
302
- harness_metrics: harness_metrics
332
+ harness_metrics: harness_metrics,
333
+ workstream: workstream_metadata
303
334
  }
304
335
  end
305
336
 
@@ -437,6 +468,88 @@ module Aidp
437
468
  }
438
469
  end
439
470
 
471
+ # Workstream management methods
472
+
473
+ # Get current workstream slug
474
+ def current_workstream
475
+ # In test mode, use instance variable
476
+ if ENV["RACK_ENV"] == "test" || defined?(RSpec)
477
+ return @test_workstream
478
+ end
479
+
480
+ state = load_state
481
+ state[:current_workstream]
482
+ end
483
+
484
+ # Get current workstream path (or project_dir if none)
485
+ def current_workstream_path
486
+ slug = current_workstream
487
+ return @project_dir unless slug
488
+
489
+ require_relative "../worktree"
490
+ ws = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
491
+ ws ? ws[:path] : @project_dir
492
+ end
493
+
494
+ # Set current workstream
495
+ def set_workstream(slug)
496
+ require_relative "../worktree"
497
+ # Verify workstream exists
498
+ ws = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
499
+ return false unless ws
500
+
501
+ # In test mode, use instance variables
502
+ if ENV["RACK_ENV"] == "test" || defined?(RSpec)
503
+ @test_workstream = slug
504
+ @test_workstream_path = ws[:path]
505
+ @test_workstream_branch = ws[:branch]
506
+ return true
507
+ end
508
+
509
+ update_state(
510
+ current_workstream: slug,
511
+ workstream_path: ws[:path],
512
+ workstream_branch: ws[:branch]
513
+ )
514
+ true
515
+ end
516
+
517
+ # Clear current workstream (switch back to main project)
518
+ def clear_workstream
519
+ # In test mode, use instance variables
520
+ if ENV["RACK_ENV"] == "test" || defined?(RSpec)
521
+ @test_workstream = nil
522
+ @test_workstream_path = nil
523
+ @test_workstream_branch = nil
524
+ return
525
+ end
526
+
527
+ update_state(
528
+ current_workstream: nil,
529
+ workstream_path: nil,
530
+ workstream_branch: nil
531
+ )
532
+ end
533
+
534
+ # Get workstream metadata
535
+ def workstream_metadata
536
+ # In test mode, use instance variables
537
+ if ENV["RACK_ENV"] == "test" || defined?(RSpec)
538
+ return {
539
+ slug: @test_workstream,
540
+ path: @test_workstream_path,
541
+ branch: @test_workstream_branch
542
+ }
543
+ end
544
+
545
+ state = load_state
546
+ {
547
+ slug: state[:current_workstream],
548
+ path: state[:workstream_path],
549
+ branch: state[:workstream_branch]
550
+ }
551
+ end
552
+
440
553
  def get_performance_metrics
441
554
  {
442
555
  efficiency: calculate_efficiency_metrics,
@@ -550,12 +663,7 @@ module Aidp
550
663
  end
551
664
  rescue Errno::EEXIST
552
665
  # Lock file exists, wait briefly and retry
553
- require "async"
554
- if Async::Task.current?
555
- Async::Task.current.sleep(0.1)
556
- else
557
- sleep(0.1)
558
- end
666
+ sleep(0.1)
559
667
  end
560
668
  end
561
669
 
@@ -54,24 +54,17 @@ module Aidp
54
54
  @display_mode = display_mode
55
55
  @last_update = Time.now
56
56
 
57
- # Start status display using Async (skip in test mode)
58
57
  unless ENV["RACK_ENV"] == "test" || defined?(RSpec)
59
- require "async"
60
- Async do |task|
61
- task.async do
62
- while @running
63
- begin
64
- collect_status_data
65
- display_status
66
- check_alerts
67
- if ENV["RACK_ENV"] == "test" || defined?(RSpec)
68
- sleep(@update_interval)
69
- else
70
- Async::Task.current.sleep(@update_interval)
71
- end
72
- rescue => e
73
- handle_display_error(e)
74
- end
58
+ require "concurrent"
59
+ @status_future = Concurrent::Future.execute do
60
+ while @running
61
+ begin
62
+ collect_status_data
63
+ display_status
64
+ check_alerts
65
+ sleep(@update_interval)
66
+ rescue => e
67
+ handle_display_error(e)
75
68
  end
76
69
  end
77
70
  end
@@ -81,7 +74,7 @@ module Aidp
81
74
  # Stop status updates
82
75
  def stop_status_updates
83
76
  @running = false
84
- @status_thread&.join
77
+ @status_future&.wait(5)
85
78
  clear_display
86
79
  end
87
80
 
@@ -16,6 +16,7 @@ module Aidp
16
16
  super(ui_components)
17
17
  @title = title
18
18
  @parent_menu = parent_menu
19
+ @ui_components = ui_components
19
20
  @submenu_items = []
20
21
  @drill_down_enabled = true
21
22
  @max_depth = 5
@@ -265,7 +265,7 @@ module Aidp
265
265
  def control_interface_loop
266
266
  loop do
267
267
  handle_control_input
268
- sleep(0.1) # Small delay to prevent excessive CPU usage
268
+ sleep(0.1)
269
269
  rescue => e
270
270
  @status_manager.show_error_status("Control interface error: #{e.message}")
271
271
  end