aidp 0.22.0 → 0.24.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +145 -31
  3. data/lib/aidp/cli.rb +19 -2
  4. data/lib/aidp/execute/work_loop_runner.rb +252 -45
  5. data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
  6. data/lib/aidp/harness/condition_detector.rb +42 -8
  7. data/lib/aidp/harness/config_manager.rb +7 -0
  8. data/lib/aidp/harness/config_schema.rb +25 -0
  9. data/lib/aidp/harness/configuration.rb +69 -6
  10. data/lib/aidp/harness/error_handler.rb +117 -44
  11. data/lib/aidp/harness/provider_manager.rb +64 -0
  12. data/lib/aidp/harness/provider_metrics.rb +138 -0
  13. data/lib/aidp/harness/runner.rb +110 -35
  14. data/lib/aidp/harness/simple_user_interface.rb +4 -0
  15. data/lib/aidp/harness/state/ui_state.rb +0 -10
  16. data/lib/aidp/harness/state_manager.rb +1 -15
  17. data/lib/aidp/harness/test_runner.rb +39 -2
  18. data/lib/aidp/logger.rb +34 -4
  19. data/lib/aidp/providers/adapter.rb +241 -0
  20. data/lib/aidp/providers/anthropic.rb +75 -7
  21. data/lib/aidp/providers/base.rb +29 -1
  22. data/lib/aidp/providers/capability_registry.rb +205 -0
  23. data/lib/aidp/providers/codex.rb +14 -0
  24. data/lib/aidp/providers/error_taxonomy.rb +195 -0
  25. data/lib/aidp/providers/gemini.rb +3 -2
  26. data/lib/aidp/setup/devcontainer/backup_manager.rb +11 -4
  27. data/lib/aidp/setup/provider_registry.rb +107 -0
  28. data/lib/aidp/setup/wizard.rb +189 -31
  29. data/lib/aidp/version.rb +1 -1
  30. data/lib/aidp/watch/build_processor.rb +357 -27
  31. data/lib/aidp/watch/plan_generator.rb +16 -1
  32. data/lib/aidp/watch/plan_processor.rb +54 -3
  33. data/lib/aidp/watch/repository_client.rb +78 -4
  34. data/lib/aidp/watch/repository_safety_checker.rb +12 -3
  35. data/lib/aidp/watch/runner.rb +52 -10
  36. data/lib/aidp/workflows/guided_agent.rb +53 -0
  37. data/lib/aidp/worktree.rb +67 -10
  38. data/templates/work_loop/decide_whats_next.md +21 -0
  39. data/templates/work_loop/diagnose_failures.md +21 -0
  40. metadata +10 -3
  41. /data/{bin → exe}/aidp +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "tty-prompt"
4
4
  require_relative "provider_factory"
5
+ require_relative "provider_metrics"
5
6
  require_relative "../rescue_logging"
6
7
  require_relative "../concurrency"
7
8
 
@@ -40,6 +41,20 @@ module Aidp
40
41
  @unavailable_cache = {}
41
42
  @binary_check_cache = {}
42
43
  @binary_check_ttl = 300 # seconds
44
+
45
+ # Initialize persistence
46
+ project_dir = if configuration.respond_to?(:project_dir)
47
+ configuration.project_dir
48
+ elsif configuration.respond_to?(:root_dir)
49
+ configuration.root_dir
50
+ else
51
+ Dir.pwd
52
+ end
53
+ @metrics_persistence = ProviderMetrics.new(project_dir)
54
+
55
+ # Load persisted metrics
56
+ load_persisted_metrics
57
+
43
58
  initialize_fallback_chains
44
59
  initialize_provider_health
45
60
  initialize_model_configs
@@ -932,6 +947,9 @@ module Aidp
932
947
  # Update provider health
933
948
  update_provider_health(provider_name, "rate_limited")
934
949
 
950
+ # Persist rate limit info to disk
951
+ save_persisted_rate_limits
952
+
935
953
  # Switch to next provider if current one is rate limited
936
954
  if provider_name == current_provider
937
955
  switch_provider("rate_limit", {provider: provider_name})
@@ -996,6 +1014,9 @@ module Aidp
996
1014
  metrics[:last_error_time] = Time.now
997
1015
  update_provider_health(provider_name, "error", {error: error})
998
1016
  end
1017
+
1018
+ # Persist metrics to disk
1019
+ save_persisted_metrics
999
1020
  end
1000
1021
 
1001
1022
  # Record model metrics
@@ -1611,6 +1632,49 @@ module Aidp
1611
1632
  # Most models reset rate limits every hour
1612
1633
  Time.now + (60 * 60)
1613
1634
  end
1635
+
1636
+ # Load persisted metrics from disk
1637
+ def load_persisted_metrics
1638
+ return unless @metrics_persistence
1639
+
1640
+ # Load provider metrics
1641
+ persisted_metrics = @metrics_persistence.load_metrics
1642
+ @provider_metrics.merge!(persisted_metrics) if persisted_metrics.is_a?(Hash)
1643
+
1644
+ # Load rate limit info
1645
+ persisted_rate_limits = @metrics_persistence.load_rate_limits
1646
+ @rate_limit_info.merge!(persisted_rate_limits) if persisted_rate_limits.is_a?(Hash)
1647
+
1648
+ # Clean up expired rate limits
1649
+ cleanup_expired_rate_limits
1650
+ rescue => e
1651
+ log_rescue(e, component: "provider_manager", action: "load_persisted_metrics", fallback: nil)
1652
+ end
1653
+
1654
+ # Save persisted metrics to disk
1655
+ def save_persisted_metrics
1656
+ return unless @metrics_persistence
1657
+ @metrics_persistence.save_metrics(@provider_metrics)
1658
+ rescue => e
1659
+ log_rescue(e, component: "provider_manager", action: "save_persisted_metrics", fallback: nil)
1660
+ end
1661
+
1662
+ # Save persisted rate limits to disk
1663
+ def save_persisted_rate_limits
1664
+ return unless @metrics_persistence
1665
+ @metrics_persistence.save_rate_limits(@rate_limit_info)
1666
+ rescue => e
1667
+ log_rescue(e, component: "provider_manager", action: "save_persisted_rate_limits", fallback: nil)
1668
+ end
1669
+
1670
+ # Clean up expired rate limits from memory
1671
+ def cleanup_expired_rate_limits
1672
+ now = Time.now
1673
+ @rate_limit_info.delete_if do |_provider, info|
1674
+ reset_time = info[:reset_time]
1675
+ reset_time && now >= reset_time
1676
+ end
1677
+ end
1614
1678
  end
1615
1679
  end
1616
1680
  end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+ require_relative "../rescue_logging"
6
+
7
+ module Aidp
8
+ module Harness
9
+ # Persists provider metrics and rate limit information to disk
10
+ # Enables the provider dashboard to display real-time state
11
+ class ProviderMetrics
12
+ include Aidp::RescueLogging
13
+
14
+ attr_reader :project_dir, :metrics_file, :rate_limit_file
15
+
16
+ def initialize(project_dir)
17
+ @project_dir = project_dir
18
+ @metrics_file = File.join(project_dir, ".aidp", "provider_metrics.yml")
19
+ @rate_limit_file = File.join(project_dir, ".aidp", "provider_rate_limits.yml")
20
+ ensure_directory
21
+ end
22
+
23
+ # Save provider metrics to disk
24
+ def save_metrics(metrics_hash)
25
+ return if metrics_hash.nil? || metrics_hash.empty?
26
+
27
+ # Convert Time objects to ISO8601 strings for YAML serialization
28
+ serializable_metrics = serialize_metrics(metrics_hash)
29
+
30
+ File.write(@metrics_file, YAML.dump(serializable_metrics))
31
+ rescue => e
32
+ log_rescue(e, component: "provider_metrics", action: "save_metrics", fallback: nil)
33
+ end
34
+
35
+ # Load provider metrics from disk
36
+ def load_metrics
37
+ return {} unless File.exist?(@metrics_file)
38
+
39
+ data = YAML.safe_load_file(@metrics_file, permitted_classes: [Time, Date, Symbol], aliases: true)
40
+ return {} unless data.is_a?(Hash)
41
+
42
+ # Convert ISO8601 strings back to Time objects
43
+ deserialize_metrics(data)
44
+ rescue => e
45
+ log_rescue(e, component: "provider_metrics", action: "load_metrics", fallback: {})
46
+ {}
47
+ end
48
+
49
+ # Save rate limit information to disk
50
+ def save_rate_limits(rate_limit_hash)
51
+ return if rate_limit_hash.nil? || rate_limit_hash.empty?
52
+
53
+ # Convert Time objects to ISO8601 strings for YAML serialization
54
+ serializable_rate_limits = serialize_rate_limits(rate_limit_hash)
55
+
56
+ File.write(@rate_limit_file, YAML.dump(serializable_rate_limits))
57
+ rescue => e
58
+ log_rescue(e, component: "provider_metrics", action: "save_rate_limits", fallback: nil)
59
+ end
60
+
61
+ # Load rate limit information from disk
62
+ def load_rate_limits
63
+ return {} unless File.exist?(@rate_limit_file)
64
+
65
+ data = YAML.safe_load_file(@rate_limit_file, permitted_classes: [Time, Date, Symbol], aliases: true)
66
+ return {} unless data.is_a?(Hash)
67
+
68
+ # Convert ISO8601 strings back to Time objects
69
+ deserialize_rate_limits(data)
70
+ rescue => e
71
+ log_rescue(e, component: "provider_metrics", action: "load_rate_limits", fallback: {})
72
+ {}
73
+ end
74
+
75
+ # Clear all persisted metrics
76
+ def clear
77
+ File.delete(@metrics_file) if File.exist?(@metrics_file)
78
+ File.delete(@rate_limit_file) if File.exist?(@rate_limit_file)
79
+ end
80
+
81
+ private
82
+
83
+ def ensure_directory
84
+ aidp_dir = File.join(@project_dir, ".aidp")
85
+ FileUtils.mkdir_p(aidp_dir) unless File.directory?(aidp_dir)
86
+ end
87
+
88
+ def serialize_metrics(metrics_hash)
89
+ metrics_hash.transform_values do |provider_metrics|
90
+ next provider_metrics unless provider_metrics.is_a?(Hash)
91
+
92
+ provider_metrics.transform_values do |value|
93
+ value.is_a?(Time) ? value.iso8601 : value
94
+ end
95
+ end
96
+ end
97
+
98
+ def deserialize_metrics(metrics_hash)
99
+ metrics_hash.transform_values do |provider_metrics|
100
+ next provider_metrics unless provider_metrics.is_a?(Hash)
101
+
102
+ provider_metrics.transform_keys(&:to_sym).transform_values do |value|
103
+ parse_time_if_string(value)
104
+ end
105
+ end
106
+ end
107
+
108
+ def serialize_rate_limits(rate_limit_hash)
109
+ rate_limit_hash.transform_values do |limit_info|
110
+ next limit_info unless limit_info.is_a?(Hash)
111
+
112
+ limit_info.transform_values do |value|
113
+ value.is_a?(Time) ? value.iso8601 : value
114
+ end
115
+ end
116
+ end
117
+
118
+ def deserialize_rate_limits(rate_limit_hash)
119
+ rate_limit_hash.transform_values do |limit_info|
120
+ next limit_info unless limit_info.is_a?(Hash)
121
+
122
+ limit_info.transform_keys(&:to_sym).transform_values do |value|
123
+ parse_time_if_string(value)
124
+ end
125
+ end
126
+ end
127
+
128
+ def parse_time_if_string(value)
129
+ return value unless value.is_a?(String)
130
+
131
+ # Try to parse ISO8601 timestamp
132
+ Time.parse(value)
133
+ rescue ArgumentError
134
+ value
135
+ end
136
+ end
137
+ end
138
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "timeout"
4
4
  require "json"
5
- require_relative "config_manager"
5
+ require_relative "configuration"
6
6
  require_relative "state_manager"
7
7
  require_relative "condition_detector"
8
8
  require_relative "provider_manager"
@@ -27,11 +27,12 @@ module Aidp
27
27
  waiting_for_rate_limit: "waiting_for_rate_limit",
28
28
  stopped: "stopped",
29
29
  completed: "completed",
30
- error: "error"
30
+ error: "error",
31
+ needs_clarification: "needs_clarification"
31
32
  }.freeze
32
33
 
33
34
  # Public accessors for testing and integration
34
- attr_reader :current_provider, :current_step, :user_input, :execution_log, :provider_manager
35
+ attr_reader :current_provider, :current_step, :user_input, :execution_log, :provider_manager, :clarification_questions
35
36
 
36
37
  def initialize(project_dir, mode = :analyze, options = {})
37
38
  @project_dir = project_dir
@@ -43,27 +44,31 @@ module Aidp
43
44
  @current_provider = nil
44
45
  @user_input = options[:user_input] || {} # Include user input from workflow selection
45
46
  @execution_log = []
47
+ @last_error = nil
46
48
  @prompt = options[:prompt] || TTY::Prompt.new
47
49
 
48
50
  # Store workflow configuration
49
51
  @selected_steps = options[:selected_steps]
50
52
  @workflow_type = options[:workflow_type]
53
+ @non_interactive = options[:non_interactive] || (@workflow_type == :watch_mode)
51
54
 
52
55
  # Initialize components
53
- @config_manager = ConfigManager.new(project_dir)
56
+ @configuration = Configuration.new(project_dir)
54
57
  @state_manager = StateManager.new(project_dir, @mode)
55
- @provider_manager = ProviderManager.new(@config_manager, prompt: @prompt)
58
+ @provider_manager = ProviderManager.new(@configuration, prompt: @prompt)
56
59
 
57
60
  # Use ZFC-enabled condition detector
58
61
  # ZfcConditionDetector will create its own ProviderFactory if needed
59
62
  # Falls back to legacy pattern matching when ZFC is disabled
60
63
  require_relative "zfc_condition_detector"
61
- @condition_detector = ZfcConditionDetector.new(@config_manager)
64
+ @condition_detector = ZfcConditionDetector.new(@configuration)
62
65
 
63
66
  @user_interface = SimpleUserInterface.new
64
- @error_handler = ErrorHandler.new(@provider_manager, @config_manager)
67
+ @error_handler = ErrorHandler.new(@provider_manager, @configuration)
65
68
  @status_display = StatusDisplay.new
66
69
  @completion_checker = CompletionChecker.new(@project_dir, @workflow_type)
70
+ @failure_reason = nil
71
+ @failure_metadata = nil
67
72
  end
68
73
 
69
74
  # Main execution method - runs the harness loop
@@ -113,26 +118,56 @@ module Aidp
113
118
  display_message(completion_status[:summary], type: :info)
114
119
 
115
120
  # Ask user if they want to continue anyway
116
- if @user_interface.get_confirmation("Continue anyway? This may indicate issues that should be addressed.", default: false)
117
- @state = STATES[:completed]
118
- log_execution("Harness completed with user override")
121
+ if confirmation_prompt_allowed?
122
+ if @user_interface.get_confirmation("Continue anyway? This may indicate issues that should be addressed.", default: false)
123
+ @state = STATES[:completed]
124
+ log_execution("Harness completed with user override")
125
+ else
126
+ mark_completion_failure(completion_status)
127
+ @state = STATES[:error]
128
+ log_execution("Harness stopped due to unmet completion criteria")
129
+ end
119
130
  else
131
+ display_message("⚠️ Non-interactive mode: cannot override failed completion criteria. Stopping run.", type: :warning)
132
+ mark_completion_failure(completion_status)
120
133
  @state = STATES[:error]
121
- log_execution("Harness stopped due to unmet completion criteria")
134
+ log_execution("Harness stopped due to unmet completion criteria in non-interactive mode")
122
135
  end
123
136
  end
124
137
  end
125
138
  rescue => e
126
139
  @state = STATES[:error]
127
- log_execution("Harness error: #{e.message}", {error: e.class.name})
140
+ @last_error = e
141
+ log_execution("Harness error: #{e.message}", {error: e.class.name, backtrace: e.backtrace&.first(5)})
128
142
  handle_error(e)
129
143
  ensure
130
- # Save state before exiting
131
- save_state
132
- cleanup
144
+ # Save state before exiting - protect against exceptions during cleanup
145
+ begin
146
+ save_state
147
+ rescue => e
148
+ # Don't let state save failures kill the whole run or prevent cleanup
149
+ Aidp.logger.error("harness", "Failed to save state during cleanup: #{e.message}", error: e.class.name)
150
+ @last_error ||= e # Only set if no previous error
151
+ end
152
+
153
+ begin
154
+ cleanup
155
+ rescue => e
156
+ # Don't let cleanup failures propagate
157
+ Aidp.logger.error("harness", "Failed during cleanup: #{e.message}", error: e.class.name)
158
+ end
133
159
  end
134
160
 
135
- {status: @state, message: get_completion_message}
161
+ result = {status: @state, message: get_completion_message}
162
+ result[:reason] = @failure_reason if @failure_reason
163
+ result[:failure_metadata] = @failure_metadata if @failure_metadata
164
+ result[:clarification_questions] = @clarification_questions if @clarification_questions
165
+ if @last_error
166
+ result[:error] = @last_error.message
167
+ result[:error_class] = @last_error.class.name
168
+ result[:backtrace] = @last_error.backtrace&.first(10)
169
+ end
170
+ result
136
171
  end
137
172
 
138
173
  # Pause the harness execution
@@ -180,9 +215,9 @@ module Aidp
180
215
  {
181
216
  harness: status,
182
217
  configuration: {
183
- default_provider: @config_manager.default_provider,
184
- fallback_providers: @config_manager.fallback_providers,
185
- max_retries: @config_manager.harness_config[:max_retries]
218
+ default_provider: @configuration.default_provider,
219
+ fallback_providers: @configuration.fallback_providers,
220
+ max_retries: @configuration.harness_config[:max_retries]
186
221
  },
187
222
  provider_manager: @provider_manager.status,
188
223
  error_stats: @error_handler.error_stats
@@ -248,12 +283,23 @@ module Aidp
248
283
  end
249
284
 
250
285
  def handle_user_feedback_request(result)
251
- @state = STATES[:waiting_for_user]
252
- log_execution("Waiting for user feedback")
253
-
254
286
  # Extract questions from result
255
287
  questions = @condition_detector.extract_questions(result)
256
288
 
289
+ # Check if we're in watch mode (non-interactive)
290
+ if @options[:workflow_type] == :watch_mode
291
+ # Store questions for later retrieval and set state to needs_clarification
292
+ @clarification_questions = questions
293
+ @state = STATES[:needs_clarification]
294
+ log_execution("Clarification needed in watch mode", {question_count: questions.size})
295
+ # Don't continue - exit the loop so we can return this status
296
+ return
297
+ end
298
+
299
+ # Interactive mode: collect feedback from user
300
+ @state = STATES[:waiting_for_user]
301
+ log_execution("Waiting for user feedback")
302
+
257
303
  # Collect user input
258
304
  user_responses = @user_interface.collect_feedback(questions)
259
305
 
@@ -267,15 +313,30 @@ module Aidp
267
313
  log_execution("User feedback collected", {responses: user_responses.keys})
268
314
  end
269
315
 
270
- def handle_rate_limit(_result)
316
+ def handle_rate_limit(result)
271
317
  @state = STATES[:waiting_for_rate_limit]
272
318
  log_execution("Rate limit detected, switching provider")
273
319
 
320
+ rate_limit_info = nil
321
+ if @condition_detector.respond_to?(:extract_rate_limit_info)
322
+ rate_limit_info = @condition_detector.extract_rate_limit_info(result, @current_provider)
323
+ end
324
+ reset_time = rate_limit_info && rate_limit_info[:reset_time]
325
+
274
326
  # Mark current provider as rate limited
275
- @provider_manager.mark_rate_limited(@current_provider)
327
+ @provider_manager.mark_rate_limited(@current_provider, reset_time)
328
+
329
+ # Provider manager might already have switched upstream (e.g., during CLI execution)
330
+ manager_current = @provider_manager.current_provider
331
+ if manager_current && manager_current != @current_provider
332
+ @current_provider = manager_current
333
+ @state = STATES[:running]
334
+ log_execution("Provider already switched upstream", new_provider: manager_current)
335
+ return
336
+ end
276
337
 
277
- # Switch to next provider
278
- next_provider = @provider_manager.switch_provider
338
+ # Switch to next provider explicitly when still on the rate-limited provider
339
+ next_provider = @provider_manager.switch_provider("rate_limit", previous_provider: @current_provider)
279
340
  @current_provider = next_provider
280
341
 
281
342
  if next_provider
@@ -299,6 +360,10 @@ module Aidp
299
360
  end
300
361
  end
301
362
 
363
+ def confirmation_prompt_allowed?
364
+ !@non_interactive
365
+ end
366
+
302
367
  def sleep_until_reset(reset_time)
303
368
  while Time.now < reset_time && @state == STATES[:waiting_for_rate_limit]
304
369
  remaining = reset_time - Time.now
@@ -360,20 +425,14 @@ module Aidp
360
425
  end
361
426
 
362
427
  def save_state
363
- # Save harness-specific state
428
+ # Save harness-specific state (execution_log removed to prevent unbounded growth)
364
429
  @state_manager.save_state({
365
430
  state: @state,
366
431
  current_step: @current_step,
367
432
  current_provider: @current_provider,
368
433
  user_input: @user_input,
369
- execution_log: @execution_log,
370
434
  last_saved: Time.now
371
435
  })
372
-
373
- # Also save execution log entries to state manager
374
- @execution_log.each do |entry|
375
- @state_manager.add_execution_log(entry)
376
- end
377
436
  end
378
437
 
379
438
  def handle_error(error)
@@ -386,6 +445,7 @@ module Aidp
386
445
  end
387
446
 
388
447
  def log_execution(message, data = {})
448
+ # Keep in-memory log for runtime diagnostics (not persisted)
389
449
  log_entry = {
390
450
  timestamp: Time.now,
391
451
  message: message,
@@ -394,7 +454,13 @@ module Aidp
394
454
  }
395
455
  @execution_log << log_entry
396
456
 
397
- # Also log to standard logging if available
457
+ # Log to persistent logger instead of state file
458
+ Aidp.logger.info("harness_execution", message,
459
+ state: @state,
460
+ step: @current_step,
461
+ **data.slice(:error, :error_class, :criteria, :all_complete, :summary).compact)
462
+
463
+ # Also log to standard output in debug mode
398
464
  puts "[#{Time.now.strftime("%H:%M:%S")}] #{message}" if ENV["AIDP_DEBUG"] == "1"
399
465
  end
400
466
 
@@ -405,12 +471,21 @@ module Aidp
405
471
  when STATES[:stopped]
406
472
  "Harness stopped by user."
407
473
  when STATES[:error]
408
- "Harness encountered an error and stopped."
474
+ if @last_error
475
+ "Harness encountered an error and stopped: #{@last_error.class.name}: #{@last_error.message}"
476
+ else
477
+ "Harness encountered an error and stopped."
478
+ end
409
479
  else
410
480
  "Harness finished in state: #{@state}"
411
481
  end
412
482
  end
413
483
 
484
+ def mark_completion_failure(completion_status)
485
+ @failure_reason = :completion_criteria
486
+ @failure_metadata = completion_status
487
+ end
488
+
414
489
  private
415
490
  end
416
491
  end
@@ -26,6 +26,10 @@ module Aidp
26
26
  responses
27
27
  end
28
28
 
29
+ def get_confirmation(message, default: true)
30
+ @prompt.yes?(message, default: default)
31
+ end
32
+
29
33
  private
30
34
 
31
35
  def show_context(context)
@@ -19,16 +19,6 @@ module Aidp
19
19
  update_state(user_input: current_input)
20
20
  end
21
21
 
22
- def execution_log
23
- state[:execution_log] || []
24
- end
25
-
26
- def add_execution_log(entry)
27
- current_log = execution_log
28
- current_log << entry
29
- update_state(execution_log: current_log)
30
- end
31
-
32
22
  def current_step
33
23
  state[:current_step]
34
24
  end
@@ -129,20 +129,6 @@ module Aidp
129
129
  update_state(user_input: current_input, last_updated: Time.now)
130
130
  end
131
131
 
132
- # Get execution log
133
- def execution_log
134
- state = load_state
135
- return [] unless state
136
- state[:execution_log] || []
137
- end
138
-
139
- # Add to execution log
140
- def add_execution_log(entry)
141
- current_log = execution_log
142
- current_log << entry
143
- update_state(execution_log: current_log, last_updated: Time.now)
144
- end
145
-
146
132
  # Get provider state
147
133
  def provider_state
148
134
  state = load_state
@@ -615,7 +601,7 @@ module Aidp
615
601
  yield
616
602
  end
617
603
  end
618
- rescue Aidp::Concurrency::MaxAttemptsExceededError
604
+ rescue Aidp::Concurrency::MaxAttemptsError
619
605
  raise "Could not acquire state lock within timeout"
620
606
  ensure
621
607
  # Clean up lock file
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
+ require_relative "../tooling_detector"
4
5
 
5
6
  module Aidp
6
7
  module Harness
@@ -15,7 +16,7 @@ module Aidp
15
16
  # Run all configured tests
16
17
  # Returns: { success: boolean, output: string, failures: array }
17
18
  def run_tests
18
- test_commands = @config.test_commands || []
19
+ test_commands = resolved_test_commands
19
20
  return {success: true, output: "", failures: []} if test_commands.empty?
20
21
 
21
22
  results = test_commands.map { |cmd| execute_command(cmd, "test") }
@@ -25,7 +26,7 @@ module Aidp
25
26
  # Run all configured linters
26
27
  # Returns: { success: boolean, output: string, failures: array }
27
28
  def run_linters
28
- lint_commands = @config.lint_commands || []
29
+ lint_commands = resolved_lint_commands
29
30
  return {success: true, output: "", failures: []} if lint_commands.empty?
30
31
 
31
32
  results = lint_commands.map { |cmd| execute_command(cmd, "linter") }
@@ -78,6 +79,42 @@ module Aidp
78
79
 
79
80
  output.join("\n")
80
81
  end
82
+
83
+ def resolved_test_commands
84
+ explicit = Array(@config.test_commands).compact.map(&:strip).reject(&:empty?)
85
+ return explicit unless explicit.empty?
86
+
87
+ detected = detected_tooling.test_commands
88
+ log_fallback(:tests, detected) unless detected.empty?
89
+ detected
90
+ end
91
+
92
+ def resolved_lint_commands
93
+ explicit = Array(@config.lint_commands).compact.map(&:strip).reject(&:empty?)
94
+ return explicit unless explicit.empty?
95
+
96
+ detected = detected_tooling.lint_commands
97
+ log_fallback(:linters, detected) unless detected.empty?
98
+ detected
99
+ end
100
+
101
+ def detected_tooling
102
+ @detected_tooling ||= Aidp::ToolingDetector.detect(@project_dir)
103
+ rescue => e
104
+ Aidp.log_warn("test_runner", "tooling_detection_failed", error: e.message)
105
+ Aidp::ToolingDetector::Result.new(test_commands: [], lint_commands: [])
106
+ end
107
+
108
+ def log_fallback(type, commands)
109
+ Aidp.log_info(
110
+ "test_runner",
111
+ "auto_detected_commands",
112
+ category: type,
113
+ commands: commands
114
+ )
115
+ rescue NameError
116
+ # Logging infrastructure not available in some tests
117
+ end
81
118
  end
82
119
  end
83
120
  end