aidp 0.24.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 (39) 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 +165 -1
  16. data/lib/aidp/harness/config_schema.rb +50 -0
  17. data/lib/aidp/harness/provider_factory.rb +2 -0
  18. data/lib/aidp/message_display.rb +10 -2
  19. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
  20. data/lib/aidp/provider_manager.rb +2 -0
  21. data/lib/aidp/providers/kilocode.rb +202 -0
  22. data/lib/aidp/setup/provider_registry.rb +15 -0
  23. data/lib/aidp/setup/wizard.rb +12 -4
  24. data/lib/aidp/skills/composer.rb +4 -0
  25. data/lib/aidp/skills/loader.rb +3 -1
  26. data/lib/aidp/version.rb +1 -1
  27. data/lib/aidp/watch/build_processor.rb +66 -16
  28. data/lib/aidp/watch/ci_fix_processor.rb +448 -0
  29. data/lib/aidp/watch/plan_processor.rb +12 -2
  30. data/lib/aidp/watch/repository_client.rb +380 -0
  31. data/lib/aidp/watch/review_processor.rb +266 -0
  32. data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
  33. data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
  34. data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
  35. data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
  36. data/lib/aidp/watch/runner.rb +185 -0
  37. data/lib/aidp/watch/state_store.rb +53 -0
  38. data/lib/aidp.rb +1 -0
  39. metadata +20 -1
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module AutoUpdate
5
+ # Base error for all auto-update errors
6
+ class UpdateError < StandardError; end
7
+
8
+ # Error raised when too many consecutive update failures detected
9
+ class UpdateLoopError < UpdateError; end
10
+
11
+ # Error raised when checkpoint is invalid or corrupted
12
+ class CheckpointError < UpdateError; end
13
+
14
+ # Error raised when version policy prevents update
15
+ class VersionPolicyError < UpdateError; end
16
+ end
17
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "time"
6
+ require_relative "../safe_directory"
7
+
8
+ module Aidp
9
+ module AutoUpdate
10
+ # Service for tracking update failures to prevent restart loops
11
+ class FailureTracker
12
+ include Aidp::SafeDirectory
13
+
14
+ attr_reader :state_file, :max_failures
15
+
16
+ def initialize(project_dir: Dir.pwd, max_failures: 3)
17
+ @project_dir = project_dir
18
+ state_dir = File.join(project_dir, ".aidp")
19
+ actual_dir = safe_mkdir_p(state_dir, component_name: "FailureTracker")
20
+ @state_file = File.join(actual_dir, "auto_update_failures.json")
21
+ @max_failures = max_failures
22
+ @state = load_state
23
+ end
24
+
25
+ # Record a failure
26
+ def record_failure
27
+ @state[:failures] << {
28
+ timestamp: Time.now.utc.iso8601,
29
+ version: Aidp::VERSION
30
+ }
31
+
32
+ # Keep only recent failures (last hour)
33
+ @state[:failures].select! { |f|
34
+ Time.parse(f[:timestamp]) > Time.now - 3600
35
+ }
36
+
37
+ save_state
38
+
39
+ Aidp.log_warn("failure_tracker", "failure_recorded",
40
+ total_failures: @state[:failures].size,
41
+ max_failures: @max_failures)
42
+ rescue => e
43
+ Aidp.log_error("failure_tracker", "record_failure_failed",
44
+ error: e.message)
45
+ end
46
+
47
+ # Check if too many consecutive failures have occurred
48
+ # @return [Boolean]
49
+ def too_many_failures?
50
+ failure_count = @state[:failures].size
51
+ is_looping = failure_count >= @max_failures
52
+
53
+ if is_looping
54
+ Aidp.log_error("failure_tracker", "restart_loop_detected",
55
+ failure_count: failure_count,
56
+ max_failures: @max_failures)
57
+ end
58
+
59
+ is_looping
60
+ end
61
+
62
+ # Reset failure count after successful operation
63
+ def reset_on_success
64
+ previous_failures = @state[:failures].size
65
+
66
+ @state[:failures] = []
67
+ @state[:last_success] = Time.now.utc.iso8601
68
+ @state[:last_success_version] = Aidp::VERSION
69
+
70
+ save_state
71
+
72
+ Aidp.log_info("failure_tracker", "reset_on_success",
73
+ previous_failures: previous_failures,
74
+ version: Aidp::VERSION)
75
+ rescue => e
76
+ Aidp.log_error("failure_tracker", "reset_failed",
77
+ error: e.message)
78
+ end
79
+
80
+ # Get current failure count
81
+ # @return [Integer]
82
+ def failure_count
83
+ @state[:failures].size
84
+ end
85
+
86
+ # Get time since last success
87
+ # @return [Integer, nil] Seconds since last success, or nil if never successful
88
+ def time_since_last_success
89
+ return nil unless @state[:last_success]
90
+
91
+ Time.now - Time.parse(@state[:last_success])
92
+ rescue => e
93
+ Aidp.log_error("failure_tracker", "time_calculation_failed",
94
+ error: e.message)
95
+ nil
96
+ end
97
+
98
+ # Get all failure timestamps
99
+ # @return [Array<Time>]
100
+ def failure_timestamps
101
+ @state[:failures].map { |f| Time.parse(f[:timestamp]) }
102
+ rescue => e
103
+ Aidp.log_error("failure_tracker", "timestamp_parsing_failed",
104
+ error: e.message)
105
+ []
106
+ end
107
+
108
+ # Manually reset failures (for CLI command or recovery)
109
+ def force_reset
110
+ Aidp.log_warn("failure_tracker", "manual_reset_triggered",
111
+ previous_failures: @state[:failures].size)
112
+
113
+ @state[:failures] = []
114
+ save_state
115
+ end
116
+
117
+ # Get state summary for status display
118
+ # @return [Hash]
119
+ def status
120
+ {
121
+ failure_count: failure_count,
122
+ max_failures: @max_failures,
123
+ too_many_failures: too_many_failures?,
124
+ last_success: @state[:last_success],
125
+ last_success_version: @state[:last_success_version],
126
+ recent_failures: @state[:failures]
127
+ }
128
+ end
129
+
130
+ private
131
+
132
+ def load_state
133
+ return default_state unless File.exist?(@state_file)
134
+
135
+ JSON.parse(File.read(@state_file), symbolize_names: true)
136
+ rescue JSON::ParserError => e
137
+ Aidp.log_warn("failure_tracker", "state_file_corrupted",
138
+ error: e.message)
139
+ default_state
140
+ rescue => e
141
+ Aidp.log_warn("failure_tracker", "load_state_failed",
142
+ error: e.message)
143
+ default_state
144
+ end
145
+
146
+ def save_state
147
+ File.write(@state_file, JSON.pretty_generate(@state))
148
+ rescue => e
149
+ Aidp.log_error("failure_tracker", "save_state_failed",
150
+ error: e.message)
151
+ end
152
+
153
+ def default_state
154
+ {
155
+ failures: [],
156
+ last_success: nil,
157
+ last_success_version: nil
158
+ }
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module Aidp
8
+ module AutoUpdate
9
+ # Adapter for querying gem versions via RubyGems API (fallback)
10
+ class RubyGemsAPIAdapter
11
+ RUBYGEMS_API_BASE = "https://rubygems.org/api/v1"
12
+ TIMEOUT_SECONDS = 5
13
+
14
+ # Get the latest version of a gem from RubyGems API
15
+ # @param gem_name [String] Name of the gem
16
+ # @param allow_prerelease [Boolean] Whether to allow prerelease versions
17
+ # @return [Gem::Version, nil] Latest version or nil if unavailable
18
+ def latest_version_for(gem_name, allow_prerelease: false)
19
+ Aidp.log_debug("rubygems_api", "checking_gem_version",
20
+ gem: gem_name,
21
+ allow_prerelease: allow_prerelease)
22
+
23
+ uri = URI.parse("#{RUBYGEMS_API_BASE}/gems/#{gem_name}.json")
24
+ response = fetch_with_timeout(uri)
25
+
26
+ return nil unless response.is_a?(Net::HTTPSuccess)
27
+
28
+ data = JSON.parse(response.body)
29
+ version_string = data["version"]
30
+
31
+ if version_string
32
+ version = Gem::Version.new(version_string)
33
+
34
+ # Filter out prerelease if not allowed
35
+ if !allow_prerelease && version.prerelease?
36
+ Aidp.log_debug("rubygems_api", "skipping_prerelease",
37
+ gem: gem_name,
38
+ version: version_string)
39
+ return nil
40
+ end
41
+
42
+ Aidp.log_debug("rubygems_api", "found_version",
43
+ gem: gem_name,
44
+ version: version_string)
45
+ version
46
+ else
47
+ Aidp.log_debug("rubygems_api", "no_version_in_response",
48
+ gem: gem_name)
49
+ nil
50
+ end
51
+ rescue JSON::ParserError => e
52
+ Aidp.log_error("rubygems_api", "json_parse_failed",
53
+ gem: gem_name,
54
+ error: e.message)
55
+ nil
56
+ rescue ArgumentError => e
57
+ Aidp.log_error("rubygems_api", "invalid_version",
58
+ gem: gem_name,
59
+ error: e.message)
60
+ nil
61
+ rescue => e
62
+ Aidp.log_error("rubygems_api", "api_request_failed",
63
+ gem: gem_name,
64
+ error: e.message)
65
+ nil
66
+ end
67
+
68
+ private
69
+
70
+ def fetch_with_timeout(uri)
71
+ Net::HTTP.start(
72
+ uri.host,
73
+ uri.port,
74
+ use_ssl: uri.scheme == "https",
75
+ open_timeout: TIMEOUT_SECONDS,
76
+ read_timeout: TIMEOUT_SECONDS
77
+ ) do |http|
78
+ request = Net::HTTP::Get.new(uri)
79
+ request["User-Agent"] = "Aidp/#{Aidp::VERSION}"
80
+ http.request(request)
81
+ end
82
+ rescue Timeout::Error, Errno::ETIMEDOUT
83
+ Aidp.log_warn("rubygems_api", "request_timeout",
84
+ uri: uri.to_s,
85
+ timeout: TIMEOUT_SECONDS)
86
+ nil
87
+ rescue SocketError, Errno::ECONNREFUSED => e
88
+ Aidp.log_warn("rubygems_api", "connection_failed",
89
+ uri: uri.to_s,
90
+ error: e.message)
91
+ nil
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Aidp
6
+ module AutoUpdate
7
+ # Value object representing the result of an update check
8
+ class UpdateCheck
9
+ attr_reader :current_version, :available_version, :update_available,
10
+ :update_allowed, :policy_reason, :checked_at, :error
11
+
12
+ def initialize(
13
+ current_version:,
14
+ available_version:,
15
+ update_available:,
16
+ update_allowed:,
17
+ policy_reason: nil,
18
+ checked_at: Time.now,
19
+ error: nil
20
+ )
21
+ @current_version = current_version
22
+ @available_version = available_version
23
+ @update_available = update_available
24
+ @update_allowed = update_allowed
25
+ @policy_reason = policy_reason
26
+ @checked_at = checked_at
27
+ @error = error
28
+ end
29
+
30
+ # Create a failed update check
31
+ # @param error_message [String] Error message
32
+ # @param current_version [String] Current version
33
+ # @return [UpdateCheck]
34
+ def self.failed(error_message, current_version: Aidp::VERSION)
35
+ new(
36
+ current_version: current_version,
37
+ available_version: current_version,
38
+ update_available: false,
39
+ update_allowed: false,
40
+ policy_reason: "Update check failed",
41
+ error: error_message
42
+ )
43
+ end
44
+
45
+ # Create an unavailable update check (service temporarily unavailable)
46
+ # @param current_version [String] Current version
47
+ # @return [UpdateCheck]
48
+ def self.unavailable(current_version: Aidp::VERSION)
49
+ new(
50
+ current_version: current_version,
51
+ available_version: current_version,
52
+ update_available: false,
53
+ update_allowed: false,
54
+ policy_reason: "Update service temporarily unavailable"
55
+ )
56
+ end
57
+
58
+ # Check if update check was successful
59
+ # @return [Boolean]
60
+ def success?
61
+ @error.nil?
62
+ end
63
+
64
+ # Check if update check failed
65
+ # @return [Boolean]
66
+ def failed?
67
+ !success?
68
+ end
69
+
70
+ # Check if update should be performed
71
+ # @return [Boolean]
72
+ def should_update?
73
+ success? && @update_available && @update_allowed
74
+ end
75
+
76
+ # Convert to hash for serialization
77
+ # @return [Hash]
78
+ def to_h
79
+ {
80
+ current_version: @current_version,
81
+ available_version: @available_version,
82
+ update_available: @update_available,
83
+ update_allowed: @update_allowed,
84
+ policy_reason: @policy_reason,
85
+ checked_at: @checked_at.utc.iso8601,
86
+ error: @error
87
+ }
88
+ end
89
+
90
+ # Create from hash
91
+ # @param hash [Hash] Serialized update check
92
+ # @return [UpdateCheck]
93
+ def self.from_h(hash)
94
+ new(
95
+ current_version: hash[:current_version] || hash["current_version"],
96
+ available_version: hash[:available_version] || hash["available_version"],
97
+ update_available: hash[:update_available] || hash["update_available"],
98
+ update_allowed: hash[:update_allowed] || hash["update_allowed"],
99
+ policy_reason: hash[:policy_reason] || hash["policy_reason"],
100
+ checked_at: Time.parse(hash[:checked_at] || hash["checked_at"]),
101
+ error: hash[:error] || hash["error"]
102
+ )
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "socket"
6
+ require_relative "../safe_directory"
7
+
8
+ module Aidp
9
+ module AutoUpdate
10
+ # Service for logging update events in JSON Lines format
11
+ class UpdateLogger
12
+ include Aidp::SafeDirectory
13
+
14
+ attr_reader :log_file
15
+
16
+ def initialize(project_dir: Dir.pwd)
17
+ @project_dir = project_dir
18
+ log_dir = File.join(project_dir, ".aidp", "logs")
19
+ actual_dir = safe_mkdir_p(log_dir, component_name: "UpdateLogger")
20
+ @log_file = File.join(actual_dir, "updates.log")
21
+ end
22
+
23
+ # Log an update check
24
+ # @param update_check [UpdateCheck] Update check result
25
+ def log_check(update_check)
26
+ write_log_entry(
27
+ event: "check",
28
+ current_version: update_check.current_version,
29
+ available_version: update_check.available_version,
30
+ update_available: update_check.update_available,
31
+ update_allowed: update_check.update_allowed,
32
+ policy_reason: update_check.policy_reason,
33
+ error: update_check.error
34
+ )
35
+ end
36
+
37
+ # Log update initiation
38
+ # @param checkpoint [Checkpoint] Checkpoint created for update
39
+ # @param target_version [String] Version updating to
40
+ def log_update_initiated(checkpoint, target_version: nil)
41
+ write_log_entry(
42
+ event: "update_initiated",
43
+ checkpoint_id: checkpoint.checkpoint_id,
44
+ from_version: checkpoint.aidp_version,
45
+ to_version: target_version,
46
+ mode: checkpoint.mode
47
+ )
48
+ end
49
+
50
+ # Log successful checkpoint restoration
51
+ # @param checkpoint [Checkpoint] Checkpoint that was restored
52
+ def log_restore(checkpoint)
53
+ write_log_entry(
54
+ event: "restore",
55
+ checkpoint_id: checkpoint.checkpoint_id,
56
+ from_version: checkpoint.aidp_version,
57
+ restored_version: Aidp::VERSION,
58
+ mode: checkpoint.mode
59
+ )
60
+ end
61
+
62
+ # Log update failure
63
+ # @param reason [String] Failure reason
64
+ # @param checkpoint_id [String, nil] Associated checkpoint ID
65
+ def log_failure(reason, checkpoint_id: nil)
66
+ write_log_entry(
67
+ event: "failure",
68
+ reason: reason,
69
+ checkpoint_id: checkpoint_id,
70
+ version: Aidp::VERSION
71
+ )
72
+ end
73
+
74
+ # Log successful update completion
75
+ # @param from_version [String] Version updated from
76
+ # @param to_version [String] Version updated to
77
+ def log_success(from_version:, to_version:)
78
+ write_log_entry(
79
+ event: "success",
80
+ from_version: from_version,
81
+ to_version: to_version
82
+ )
83
+ end
84
+
85
+ # Log restart loop detection
86
+ # @param failure_count [Integer] Number of consecutive failures
87
+ def log_restart_loop(failure_count)
88
+ write_log_entry(
89
+ event: "restart_loop_detected",
90
+ failure_count: failure_count,
91
+ version: Aidp::VERSION
92
+ )
93
+ end
94
+
95
+ # Read recent update log entries
96
+ # @param limit [Integer] Maximum number of entries to return
97
+ # @return [Array<Hash>] Recent log entries
98
+ def recent_entries(limit: 10)
99
+ return [] unless File.exist?(@log_file)
100
+
101
+ entries = []
102
+ File.readlines(@log_file).reverse_each do |line|
103
+ break if entries.size >= limit
104
+ begin
105
+ entries << JSON.parse(line, symbolize_names: true)
106
+ rescue JSON::ParserError
107
+ # Skip malformed lines
108
+ end
109
+ end
110
+
111
+ entries
112
+ rescue => e
113
+ Aidp.log_error("update_logger", "read_entries_failed",
114
+ error: e.message)
115
+ []
116
+ end
117
+
118
+ private
119
+
120
+ def write_log_entry(data)
121
+ entry = data.merge(
122
+ timestamp: Time.now.utc.iso8601,
123
+ hostname: Socket.gethostname
124
+ )
125
+
126
+ # Remove nil values
127
+ entry = entry.compact
128
+
129
+ File.open(@log_file, "a") do |f|
130
+ f.puts(JSON.generate(entry))
131
+ end
132
+
133
+ Aidp.log_debug("update_logger", "log_entry_written",
134
+ event: data[:event])
135
+ rescue => e
136
+ # Log to main logger but don't fail
137
+ Aidp.log_error("update_logger", "write_failed",
138
+ event: data[:event],
139
+ error: e.message)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module AutoUpdate
5
+ # Value object representing auto-update configuration policy
6
+ class UpdatePolicy
7
+ attr_reader :enabled, :policy, :allow_prerelease, :check_interval_seconds,
8
+ :supervisor, :max_consecutive_failures
9
+
10
+ VALID_POLICIES = %w[off exact patch minor major].freeze
11
+ VALID_SUPERVISORS = %w[none supervisord s6 runit].freeze
12
+
13
+ def initialize(
14
+ enabled: false,
15
+ policy: "off",
16
+ allow_prerelease: false,
17
+ check_interval_seconds: 3600,
18
+ supervisor: "none",
19
+ max_consecutive_failures: 3
20
+ )
21
+ @enabled = enabled
22
+ @policy = validate_policy(policy)
23
+ @allow_prerelease = allow_prerelease
24
+ @check_interval_seconds = validate_interval(check_interval_seconds)
25
+ @supervisor = validate_supervisor(supervisor)
26
+ @max_consecutive_failures = validate_max_failures(max_consecutive_failures)
27
+ end
28
+
29
+ # Create from configuration hash
30
+ # @param config [Hash] Configuration hash from aidp.yml
31
+ # @return [UpdatePolicy]
32
+ def self.from_config(config)
33
+ return disabled unless config
34
+
35
+ new(
36
+ enabled: config[:enabled] || config["enabled"] || false,
37
+ policy: config[:policy] || config["policy"] || "off",
38
+ allow_prerelease: config[:allow_prerelease] || config["allow_prerelease"] || false,
39
+ check_interval_seconds: config[:check_interval_seconds] || config["check_interval_seconds"] || 3600,
40
+ supervisor: config[:supervisor] || config["supervisor"] || "none",
41
+ max_consecutive_failures: config[:max_consecutive_failures] || config["max_consecutive_failures"] || 3
42
+ )
43
+ end
44
+
45
+ # Create a disabled policy
46
+ # @return [UpdatePolicy]
47
+ def self.disabled
48
+ new(enabled: false, policy: "off")
49
+ end
50
+
51
+ # Check if updates are completely disabled
52
+ # @return [Boolean]
53
+ def disabled?
54
+ !@enabled || @policy == "off"
55
+ end
56
+
57
+ # Check if a supervisor is configured
58
+ # @return [Boolean]
59
+ def supervised?
60
+ @supervisor != "none"
61
+ end
62
+
63
+ # Convert to hash for serialization
64
+ # @return [Hash]
65
+ def to_h
66
+ {
67
+ enabled: @enabled,
68
+ policy: @policy,
69
+ allow_prerelease: @allow_prerelease,
70
+ check_interval_seconds: @check_interval_seconds,
71
+ supervisor: @supervisor,
72
+ max_consecutive_failures: @max_consecutive_failures
73
+ }
74
+ end
75
+
76
+ private
77
+
78
+ def validate_policy(policy)
79
+ unless VALID_POLICIES.include?(policy.to_s)
80
+ raise ArgumentError, "Invalid policy: #{policy}. Must be one of: #{VALID_POLICIES.join(", ")}"
81
+ end
82
+ policy.to_s
83
+ end
84
+
85
+ def validate_supervisor(supervisor)
86
+ unless VALID_SUPERVISORS.include?(supervisor.to_s)
87
+ raise ArgumentError, "Invalid supervisor: #{supervisor}. Must be one of: #{VALID_SUPERVISORS.join(", ")}"
88
+ end
89
+ supervisor.to_s
90
+ end
91
+
92
+ def validate_interval(interval)
93
+ interval = interval.to_i
94
+ if interval < 300 || interval > 86400
95
+ raise ArgumentError, "Invalid check_interval_seconds: #{interval}. Must be between 300 and 86400"
96
+ end
97
+ interval
98
+ end
99
+
100
+ def validate_max_failures(max_failures)
101
+ max_failures = max_failures.to_i
102
+ if max_failures < 1 || max_failures > 10
103
+ raise ArgumentError, "Invalid max_consecutive_failures: #{max_failures}. Must be between 1 and 10"
104
+ end
105
+ max_failures
106
+ end
107
+ end
108
+ end
109
+ end