aidp 0.23.0 → 0.25.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -1
  3. data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
  4. data/lib/aidp/auto_update/checkpoint.rb +178 -0
  5. data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
  6. data/lib/aidp/auto_update/coordinator.rb +204 -0
  7. data/lib/aidp/auto_update/errors.rb +17 -0
  8. data/lib/aidp/auto_update/failure_tracker.rb +162 -0
  9. data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
  10. data/lib/aidp/auto_update/update_check.rb +106 -0
  11. data/lib/aidp/auto_update/update_logger.rb +143 -0
  12. data/lib/aidp/auto_update/update_policy.rb +109 -0
  13. data/lib/aidp/auto_update/version_detector.rb +144 -0
  14. data/lib/aidp/auto_update.rb +52 -0
  15. data/lib/aidp/cli.rb +168 -1
  16. data/lib/aidp/execute/work_loop_runner.rb +252 -45
  17. data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
  18. data/lib/aidp/harness/condition_detector.rb +42 -8
  19. data/lib/aidp/harness/config_manager.rb +7 -0
  20. data/lib/aidp/harness/config_schema.rb +75 -0
  21. data/lib/aidp/harness/configuration.rb +69 -6
  22. data/lib/aidp/harness/error_handler.rb +117 -44
  23. data/lib/aidp/harness/provider_factory.rb +2 -0
  24. data/lib/aidp/harness/provider_manager.rb +64 -0
  25. data/lib/aidp/harness/provider_metrics.rb +138 -0
  26. data/lib/aidp/harness/runner.rb +90 -29
  27. data/lib/aidp/harness/simple_user_interface.rb +4 -0
  28. data/lib/aidp/harness/state/ui_state.rb +0 -10
  29. data/lib/aidp/harness/state_manager.rb +1 -15
  30. data/lib/aidp/harness/test_runner.rb +39 -2
  31. data/lib/aidp/logger.rb +34 -4
  32. data/lib/aidp/message_display.rb +10 -2
  33. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
  34. data/lib/aidp/provider_manager.rb +2 -0
  35. data/lib/aidp/providers/adapter.rb +241 -0
  36. data/lib/aidp/providers/anthropic.rb +75 -7
  37. data/lib/aidp/providers/base.rb +29 -1
  38. data/lib/aidp/providers/capability_registry.rb +205 -0
  39. data/lib/aidp/providers/codex.rb +14 -0
  40. data/lib/aidp/providers/error_taxonomy.rb +195 -0
  41. data/lib/aidp/providers/gemini.rb +3 -2
  42. data/lib/aidp/providers/kilocode.rb +202 -0
  43. data/lib/aidp/setup/provider_registry.rb +122 -0
  44. data/lib/aidp/setup/wizard.rb +125 -33
  45. data/lib/aidp/skills/composer.rb +4 -0
  46. data/lib/aidp/skills/loader.rb +3 -1
  47. data/lib/aidp/version.rb +1 -1
  48. data/lib/aidp/watch/build_processor.rb +323 -33
  49. data/lib/aidp/watch/ci_fix_processor.rb +448 -0
  50. data/lib/aidp/watch/plan_processor.rb +12 -2
  51. data/lib/aidp/watch/repository_client.rb +384 -4
  52. data/lib/aidp/watch/review_processor.rb +266 -0
  53. data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
  54. data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
  55. data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
  56. data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
  57. data/lib/aidp/watch/runner.rb +222 -5
  58. data/lib/aidp/watch/state_store.rb +53 -0
  59. data/lib/aidp/workflows/guided_agent.rb +53 -0
  60. data/lib/aidp/worktree.rb +67 -10
  61. data/lib/aidp.rb +1 -0
  62. data/templates/work_loop/decide_whats_next.md +21 -0
  63. data/templates/work_loop/diagnose_failures.md +21 -0
  64. metadata +29 -3
  65. /data/{bin → exe}/aidp +0 -0
@@ -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"
@@ -44,27 +44,31 @@ module Aidp
44
44
  @current_provider = nil
45
45
  @user_input = options[:user_input] || {} # Include user input from workflow selection
46
46
  @execution_log = []
47
+ @last_error = nil
47
48
  @prompt = options[:prompt] || TTY::Prompt.new
48
49
 
49
50
  # Store workflow configuration
50
51
  @selected_steps = options[:selected_steps]
51
52
  @workflow_type = options[:workflow_type]
53
+ @non_interactive = options[:non_interactive] || (@workflow_type == :watch_mode)
52
54
 
53
55
  # Initialize components
54
- @config_manager = ConfigManager.new(project_dir)
56
+ @configuration = Configuration.new(project_dir)
55
57
  @state_manager = StateManager.new(project_dir, @mode)
56
- @provider_manager = ProviderManager.new(@config_manager, prompt: @prompt)
58
+ @provider_manager = ProviderManager.new(@configuration, prompt: @prompt)
57
59
 
58
60
  # Use ZFC-enabled condition detector
59
61
  # ZfcConditionDetector will create its own ProviderFactory if needed
60
62
  # Falls back to legacy pattern matching when ZFC is disabled
61
63
  require_relative "zfc_condition_detector"
62
- @condition_detector = ZfcConditionDetector.new(@config_manager)
64
+ @condition_detector = ZfcConditionDetector.new(@configuration)
63
65
 
64
66
  @user_interface = SimpleUserInterface.new
65
- @error_handler = ErrorHandler.new(@provider_manager, @config_manager)
67
+ @error_handler = ErrorHandler.new(@provider_manager, @configuration)
66
68
  @status_display = StatusDisplay.new
67
69
  @completion_checker = CompletionChecker.new(@project_dir, @workflow_type)
70
+ @failure_reason = nil
71
+ @failure_metadata = nil
68
72
  end
69
73
 
70
74
  # Main execution method - runs the harness loop
@@ -114,27 +118,55 @@ module Aidp
114
118
  display_message(completion_status[:summary], type: :info)
115
119
 
116
120
  # Ask user if they want to continue anyway
117
- if @user_interface.get_confirmation("Continue anyway? This may indicate issues that should be addressed.", default: false)
118
- @state = STATES[:completed]
119
- 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
120
130
  else
131
+ display_message("⚠️ Non-interactive mode: cannot override failed completion criteria. Stopping run.", type: :warning)
132
+ mark_completion_failure(completion_status)
121
133
  @state = STATES[:error]
122
- log_execution("Harness stopped due to unmet completion criteria")
134
+ log_execution("Harness stopped due to unmet completion criteria in non-interactive mode")
123
135
  end
124
136
  end
125
137
  end
126
138
  rescue => e
127
139
  @state = STATES[:error]
128
- 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)})
129
142
  handle_error(e)
130
143
  ensure
131
- # Save state before exiting
132
- save_state
133
- 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
134
159
  end
135
160
 
136
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
137
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
138
170
  result
139
171
  end
140
172
 
@@ -183,9 +215,9 @@ module Aidp
183
215
  {
184
216
  harness: status,
185
217
  configuration: {
186
- default_provider: @config_manager.default_provider,
187
- fallback_providers: @config_manager.fallback_providers,
188
- 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]
189
221
  },
190
222
  provider_manager: @provider_manager.status,
191
223
  error_stats: @error_handler.error_stats
@@ -281,15 +313,30 @@ module Aidp
281
313
  log_execution("User feedback collected", {responses: user_responses.keys})
282
314
  end
283
315
 
284
- def handle_rate_limit(_result)
316
+ def handle_rate_limit(result)
285
317
  @state = STATES[:waiting_for_rate_limit]
286
318
  log_execution("Rate limit detected, switching provider")
287
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
+
288
326
  # Mark current provider as rate limited
289
- @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
290
337
 
291
- # Switch to next provider
292
- 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)
293
340
  @current_provider = next_provider
294
341
 
295
342
  if next_provider
@@ -313,6 +360,10 @@ module Aidp
313
360
  end
314
361
  end
315
362
 
363
+ def confirmation_prompt_allowed?
364
+ !@non_interactive
365
+ end
366
+
316
367
  def sleep_until_reset(reset_time)
317
368
  while Time.now < reset_time && @state == STATES[:waiting_for_rate_limit]
318
369
  remaining = reset_time - Time.now
@@ -374,20 +425,14 @@ module Aidp
374
425
  end
375
426
 
376
427
  def save_state
377
- # Save harness-specific state
428
+ # Save harness-specific state (execution_log removed to prevent unbounded growth)
378
429
  @state_manager.save_state({
379
430
  state: @state,
380
431
  current_step: @current_step,
381
432
  current_provider: @current_provider,
382
433
  user_input: @user_input,
383
- execution_log: @execution_log,
384
434
  last_saved: Time.now
385
435
  })
386
-
387
- # Also save execution log entries to state manager
388
- @execution_log.each do |entry|
389
- @state_manager.add_execution_log(entry)
390
- end
391
436
  end
392
437
 
393
438
  def handle_error(error)
@@ -400,6 +445,7 @@ module Aidp
400
445
  end
401
446
 
402
447
  def log_execution(message, data = {})
448
+ # Keep in-memory log for runtime diagnostics (not persisted)
403
449
  log_entry = {
404
450
  timestamp: Time.now,
405
451
  message: message,
@@ -408,7 +454,13 @@ module Aidp
408
454
  }
409
455
  @execution_log << log_entry
410
456
 
411
- # 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
412
464
  puts "[#{Time.now.strftime("%H:%M:%S")}] #{message}" if ENV["AIDP_DEBUG"] == "1"
413
465
  end
414
466
 
@@ -419,12 +471,21 @@ module Aidp
419
471
  when STATES[:stopped]
420
472
  "Harness stopped by user."
421
473
  when STATES[:error]
422
- "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
423
479
  else
424
480
  "Harness finished in state: #{@state}"
425
481
  end
426
482
  end
427
483
 
484
+ def mark_completion_failure(completion_status)
485
+ @failure_reason = :completion_criteria
486
+ @failure_metadata = completion_status
487
+ end
488
+
428
489
  private
429
490
  end
430
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
data/lib/aidp/logger.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "logger"
4
4
  require "json"
5
5
  require "fileutils"
6
+ require "pathname"
6
7
 
7
8
  module Aidp
8
9
  # Unified structured logger for all AIDP operations
@@ -84,12 +85,30 @@ module Aidp
84
85
  private
85
86
 
86
87
  def determine_log_level
87
- # Priority: ENV > config > default
88
- level_str = ENV["AIDP_LOG_LEVEL"] || @config[:level] || "info"
89
- level_sym = level_str.to_sym
88
+ # Priority: explicit env override > DEBUG flags > config > default
89
+ level_str = if ENV["AIDP_LOG_LEVEL"]
90
+ ENV["AIDP_LOG_LEVEL"]
91
+ elsif debug_env_enabled?
92
+ "debug"
93
+ elsif @config[:level]
94
+ @config[:level]
95
+ else
96
+ "info"
97
+ end
98
+ level_sym = level_str.to_s.to_sym
90
99
  LEVELS.key?(level_sym) ? level_sym : :info
91
100
  end
92
101
 
102
+ def debug_env_enabled?
103
+ raw = ENV["AIDP_DEBUG"] || ENV["DEBUG"]
104
+ return false if raw.nil?
105
+
106
+ normalized = raw.to_s.strip.downcase
107
+ return true if %w[true on yes debug].include?(normalized)
108
+
109
+ /\A\d+\z/.match?(normalized) ? normalized.to_i.positive? : false
110
+ end
111
+
93
112
  def should_log?(level)
94
113
  LEVELS[level] >= LEVELS[@level]
95
114
  end
@@ -106,7 +125,7 @@ module Aidp
106
125
  end
107
126
 
108
127
  def setup_logger
109
- info_path = File.join(@project_dir, INFO_LOG)
128
+ info_path = determine_log_file_path
110
129
  @logger = create_logger(info_path)
111
130
  # Emit instrumentation after logger is available (avoid recursive Aidp.log_* calls during bootstrap)
112
131
  return unless @instrument_internal
@@ -181,6 +200,17 @@ module Aidp
181
200
  JSON.generate(entry)
182
201
  end
183
202
 
203
+ def determine_log_file_path
204
+ custom = (ENV["AIDP_LOG_FILE"] || @config[:file]).to_s.strip
205
+ relative_path = custom.empty? ? INFO_LOG : custom
206
+
207
+ if Pathname.new(relative_path).absolute?
208
+ relative_path
209
+ else
210
+ File.join(@project_dir, relative_path)
211
+ end
212
+ end
213
+
184
214
  # Redaction patterns for common secrets
185
215
  REDACTION_PATTERNS = [
186
216
  # API keys and tokens (with capture groups)
@@ -25,8 +25,12 @@ module Aidp
25
25
 
26
26
  # Instance helper for displaying a colored message via TTY::Prompt
27
27
  def display_message(message, type: :info)
28
+ # Ensure message is UTF-8 encoded to handle emoji and special characters
29
+ message_str = message.to_s
30
+ message_str = message_str.force_encoding("UTF-8") if message_str.encoding.name == "ASCII-8BIT"
31
+ message_str = message_str.encode("UTF-8", invalid: :replace, undef: :replace)
28
32
  prompt = message_display_prompt
29
- prompt.say(message, color: COLOR_MAP.fetch(type, :white))
33
+ prompt.say(message_str, color: COLOR_MAP.fetch(type, :white))
30
34
  end
31
35
 
32
36
  # Provide a memoized prompt per including instance (if it defines @prompt)
@@ -41,7 +45,11 @@ module Aidp
41
45
  module ClassMethods
42
46
  # Class-level display helper (uses fresh prompt to respect $stdout changes)
43
47
  def display_message(message, type: :info)
44
- class_message_display_prompt.say(message, color: COLOR_MAP.fetch(type, :white))
48
+ # Ensure message is UTF-8 encoded to handle emoji and special characters
49
+ message_str = message.to_s
50
+ message_str = message_str.force_encoding("UTF-8") if message_str.encoding.name == "ASCII-8BIT"
51
+ message_str = message_str.encode("UTF-8", invalid: :replace, undef: :replace)
52
+ class_message_display_prompt.say(message_str, color: COLOR_MAP.fetch(type, :white))
45
53
  end
46
54
 
47
55
  private
@@ -82,7 +82,7 @@ module Aidp
82
82
  guide_path = File.join(@project_dir, "docs", "LLM_STYLE_GUIDE.md")
83
83
  return nil unless File.exist?(guide_path)
84
84
 
85
- File.read(guide_path)
85
+ File.read(guide_path, encoding: "UTF-8")
86
86
  end
87
87
 
88
88
  # Parse markdown content into fragments
@@ -90,6 +90,8 @@ module Aidp
90
90
  # @param content [String] Markdown content
91
91
  # @return [Array<Fragment>] Parsed fragments
92
92
  def parse_fragments(content)
93
+ # Ensure content is UTF-8 encoded
94
+ content = content.encode("UTF-8", invalid: :replace, undef: :replace) unless content.encoding == Encoding::UTF_8
93
95
  lines = content.lines
94
96
  current_content = []
95
97
  current_heading = nil
@@ -142,6 +142,8 @@ module Aidp
142
142
  Aidp::Providers::Anthropic.new(prompt: prompt)
143
143
  when "gemini"
144
144
  Aidp::Providers::Gemini.new(prompt: prompt)
145
+ when "kilocode"
146
+ Aidp::Providers::Kilocode.new(prompt: prompt)
145
147
  when "github_copilot"
146
148
  Aidp::Providers::GithubCopilot.new(prompt: prompt)
147
149
  when "codex"