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.
- checksums.yaml +4 -4
- data/README.md +27 -1
- data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
- data/lib/aidp/auto_update/checkpoint.rb +178 -0
- data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
- data/lib/aidp/auto_update/coordinator.rb +204 -0
- data/lib/aidp/auto_update/errors.rb +17 -0
- data/lib/aidp/auto_update/failure_tracker.rb +162 -0
- data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
- data/lib/aidp/auto_update/update_check.rb +106 -0
- data/lib/aidp/auto_update/update_logger.rb +143 -0
- data/lib/aidp/auto_update/update_policy.rb +109 -0
- data/lib/aidp/auto_update/version_detector.rb +144 -0
- data/lib/aidp/auto_update.rb +52 -0
- data/lib/aidp/cli.rb +165 -1
- data/lib/aidp/harness/config_schema.rb +50 -0
- data/lib/aidp/harness/provider_factory.rb +2 -0
- data/lib/aidp/message_display.rb +10 -2
- data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
- data/lib/aidp/provider_manager.rb +2 -0
- data/lib/aidp/providers/kilocode.rb +202 -0
- data/lib/aidp/setup/provider_registry.rb +15 -0
- data/lib/aidp/setup/wizard.rb +12 -4
- data/lib/aidp/skills/composer.rb +4 -0
- data/lib/aidp/skills/loader.rb +3 -1
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +66 -16
- data/lib/aidp/watch/ci_fix_processor.rb +448 -0
- data/lib/aidp/watch/plan_processor.rb +12 -2
- data/lib/aidp/watch/repository_client.rb +380 -0
- data/lib/aidp/watch/review_processor.rb +266 -0
- data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
- data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
- data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
- data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
- data/lib/aidp/watch/runner.rb +185 -0
- data/lib/aidp/watch/state_store.rb +53 -0
- data/lib/aidp.rb +1 -0
- 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
|