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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 348238e998f8d75ef9f23fcb5a8f4517b6fde0dd9612a4520583b03ab692cc23
|
|
4
|
+
data.tar.gz: ed8db8dbd0459211e65b76784b6074b65f2689cdb76f7b7c74bcb029d4c9f74d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac5628709db6248036d75e5873252089e497b96bac785a610bcc9b153b350714d4a5e441610501b3897cf6994d9081fa36e990da67678b86c167d5e58b048f64
|
|
7
|
+
data.tar.gz: dd660df245ffd1bb28048e4b76b231efce515e880fc8cfa088af6a25b1fcb42b98562d592880d256ade4474a3db6879567f3fb2670e750da0ee94ea5347b2df7
|
data/README.md
CHANGED
|
@@ -406,6 +406,7 @@ AIDP intelligently manages multiple providers with automatic switching:
|
|
|
406
406
|
- **Cursor CLI** - IDE-integrated provider for code-specific tasks
|
|
407
407
|
- **Gemini CLI** - Google's Gemini command-line interface for general tasks
|
|
408
408
|
- **GitHub Copilot CLI** - GitHub's AI pair programmer command-line interface
|
|
409
|
+
- **Kilocode** - Modern AI coding assistant with autonomous mode support
|
|
409
410
|
- **OpenCode** - Alternative open-source code generation provider
|
|
410
411
|
|
|
411
412
|
The system automatically switches providers when:
|
|
@@ -456,12 +457,37 @@ providers:
|
|
|
456
457
|
type: "subscription"
|
|
457
458
|
```
|
|
458
459
|
|
|
460
|
+
### Provider Installation
|
|
461
|
+
|
|
462
|
+
Each provider requires its CLI tool to be installed:
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
# Cursor CLI
|
|
466
|
+
npm install -g @cursor/cli
|
|
467
|
+
|
|
468
|
+
# Kilocode CLI
|
|
469
|
+
npm install -g @kilocode/cli
|
|
470
|
+
|
|
471
|
+
# OpenCode CLI
|
|
472
|
+
npm install -g @opencode/cli
|
|
473
|
+
|
|
474
|
+
# GitHub Copilot CLI (requires GitHub account)
|
|
475
|
+
gh extension install github/gh-copilot
|
|
476
|
+
```
|
|
477
|
+
|
|
459
478
|
### Environment Variables
|
|
460
479
|
|
|
461
480
|
```bash
|
|
462
|
-
# Set API keys
|
|
481
|
+
# Set API keys for usage-based providers
|
|
463
482
|
export AIDP_CLAUDE_API_KEY="your-claude-api-key"
|
|
464
483
|
export AIDP_GEMINI_API_KEY="your-gemini-api-key"
|
|
484
|
+
|
|
485
|
+
# Kilocode authentication (get token from kilocode.ai profile)
|
|
486
|
+
export KILOCODE_TOKEN="your-kilocode-api-token"
|
|
487
|
+
|
|
488
|
+
# Optional: Configure provider-specific settings
|
|
489
|
+
export KILOCODE_MODEL="your-preferred-model"
|
|
490
|
+
export AIDP_KILOCODE_TIMEOUT="600" # Custom timeout in seconds
|
|
465
491
|
```
|
|
466
492
|
|
|
467
493
|
## Tree-sitter Static Analysis
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module AutoUpdate
|
|
7
|
+
# Adapter for querying gem versions via Bundler
|
|
8
|
+
class BundlerAdapter
|
|
9
|
+
# Get the latest version of a gem according to bundle outdated
|
|
10
|
+
# @param gem_name [String] Name of the gem
|
|
11
|
+
# @return [Gem::Version, nil] Latest version or nil if unavailable
|
|
12
|
+
def latest_version_for(gem_name)
|
|
13
|
+
Aidp.log_debug("bundler_adapter", "checking_gem_version", gem: gem_name)
|
|
14
|
+
|
|
15
|
+
# Use mise exec to ensure correct Ruby version
|
|
16
|
+
stdout, stderr, status = Open3.capture3(
|
|
17
|
+
"mise", "exec", "--", "bundle", "outdated", gem_name, "--parseable"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
unless status.success?
|
|
21
|
+
Aidp.log_debug("bundler_adapter", "bundle_outdated_failed",
|
|
22
|
+
gem: gem_name,
|
|
23
|
+
stderr: stderr.strip)
|
|
24
|
+
return nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Parse bundle outdated output
|
|
28
|
+
# Format: "gem_name (newest version, installed version, requested version)"
|
|
29
|
+
# Example: "aidp (0.25.0, 0.24.0, >= 0)"
|
|
30
|
+
parse_bundle_outdated(stdout, gem_name)
|
|
31
|
+
rescue => e
|
|
32
|
+
Aidp.log_error("bundler_adapter", "version_check_failed",
|
|
33
|
+
gem: gem_name,
|
|
34
|
+
error: e.message)
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def parse_bundle_outdated(output, gem_name)
|
|
41
|
+
# Example output line: "aidp (newest 0.25.0, installed 0.24.0)"
|
|
42
|
+
output.each_line do |line|
|
|
43
|
+
next unless line.start_with?(gem_name)
|
|
44
|
+
|
|
45
|
+
# Extract newest version using regex
|
|
46
|
+
if line =~ /newest\s+([0-9.]+[a-z0-9.-]*)/
|
|
47
|
+
version_string = ::Regexp.last_match(1)
|
|
48
|
+
Aidp.log_debug("bundler_adapter", "found_version",
|
|
49
|
+
gem: gem_name,
|
|
50
|
+
version: version_string)
|
|
51
|
+
return Gem::Version.new(version_string)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Aidp.log_debug("bundler_adapter", "no_newer_version",
|
|
56
|
+
gem: gem_name)
|
|
57
|
+
nil
|
|
58
|
+
rescue ArgumentError => e
|
|
59
|
+
Aidp.log_error("bundler_adapter", "invalid_version",
|
|
60
|
+
gem: gem_name,
|
|
61
|
+
error: e.message)
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "digest"
|
|
7
|
+
require "open3"
|
|
8
|
+
|
|
9
|
+
module Aidp
|
|
10
|
+
module AutoUpdate
|
|
11
|
+
# Aggregate root representing a complete state snapshot for restart recovery
|
|
12
|
+
class Checkpoint
|
|
13
|
+
attr_reader :checkpoint_id, :created_at, :aidp_version, :mode, :watch_state,
|
|
14
|
+
:metadata, :checksum
|
|
15
|
+
|
|
16
|
+
SCHEMA_VERSION = 1
|
|
17
|
+
|
|
18
|
+
def initialize(
|
|
19
|
+
mode:, checkpoint_id: SecureRandom.uuid,
|
|
20
|
+
created_at: Time.now,
|
|
21
|
+
aidp_version: Aidp::VERSION,
|
|
22
|
+
watch_state: nil,
|
|
23
|
+
metadata: {},
|
|
24
|
+
checksum: nil
|
|
25
|
+
)
|
|
26
|
+
@checkpoint_id = checkpoint_id
|
|
27
|
+
@created_at = created_at
|
|
28
|
+
@aidp_version = aidp_version
|
|
29
|
+
@mode = validate_mode(mode)
|
|
30
|
+
@watch_state = watch_state
|
|
31
|
+
@metadata = default_metadata.merge(metadata)
|
|
32
|
+
@checksum = checksum || compute_checksum
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Create checkpoint from current watch mode state
|
|
36
|
+
# @param runner [Aidp::Watch::Runner] Watch mode runner
|
|
37
|
+
# @return [Checkpoint]
|
|
38
|
+
def self.from_watch_runner(runner)
|
|
39
|
+
new(
|
|
40
|
+
mode: "watch",
|
|
41
|
+
watch_state: {
|
|
42
|
+
repository: runner.instance_variable_get(:@repository_client).full_repo,
|
|
43
|
+
interval: runner.instance_variable_get(:@interval),
|
|
44
|
+
provider_name: runner.instance_variable_get(:@plan_processor).instance_variable_get(:@plan_generator).instance_variable_get(:@provider_name),
|
|
45
|
+
persona: nil, # Not currently tracked in runner
|
|
46
|
+
safety_config: runner.instance_variable_get(:@safety_checker).instance_variable_get(:@config),
|
|
47
|
+
worktree_context: capture_worktree_context,
|
|
48
|
+
state_store_snapshot: runner.instance_variable_get(:@state_store).send(:state)
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Create checkpoint from hash (deserialization)
|
|
54
|
+
# @param hash [Hash] Serialized checkpoint data
|
|
55
|
+
# @return [Checkpoint]
|
|
56
|
+
def self.from_h(hash)
|
|
57
|
+
new(
|
|
58
|
+
checkpoint_id: hash[:checkpoint_id] || hash["checkpoint_id"],
|
|
59
|
+
created_at: Time.parse(hash[:created_at] || hash["created_at"]),
|
|
60
|
+
aidp_version: hash[:aidp_version] || hash["aidp_version"],
|
|
61
|
+
mode: hash[:mode] || hash["mode"],
|
|
62
|
+
watch_state: hash[:watch_state] || hash["watch_state"],
|
|
63
|
+
metadata: hash[:metadata] || hash["metadata"] || {},
|
|
64
|
+
checksum: hash[:checksum] || hash["checksum"]
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Convert to hash for serialization
|
|
69
|
+
# @return [Hash]
|
|
70
|
+
def to_h
|
|
71
|
+
{
|
|
72
|
+
schema_version: SCHEMA_VERSION,
|
|
73
|
+
checkpoint_id: @checkpoint_id,
|
|
74
|
+
created_at: @created_at.utc.iso8601(6), # Preserve microsecond precision
|
|
75
|
+
aidp_version: @aidp_version,
|
|
76
|
+
mode: @mode,
|
|
77
|
+
watch_state: @watch_state,
|
|
78
|
+
metadata: @metadata,
|
|
79
|
+
checksum: @checksum
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Verify checkpoint integrity
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def valid?
|
|
86
|
+
@checksum == compute_checksum
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Check if checkpoint is for watch mode
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def watch_mode?
|
|
92
|
+
@mode == "watch"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check if checkpoint is compatible with current Aidp version
|
|
96
|
+
# @return [Boolean]
|
|
97
|
+
def compatible_version?
|
|
98
|
+
# Allow restoring from same major.minor version
|
|
99
|
+
checkpoint_ver = Gem::Version.new(@aidp_version)
|
|
100
|
+
current_ver = Gem::Version.new(Aidp::VERSION)
|
|
101
|
+
|
|
102
|
+
checkpoint_ver.segments[0] == current_ver.segments[0] &&
|
|
103
|
+
checkpoint_ver.segments[1] == current_ver.segments[1]
|
|
104
|
+
rescue ArgumentError
|
|
105
|
+
false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def validate_mode(mode)
|
|
111
|
+
valid_modes = %w[watch execute analyze]
|
|
112
|
+
unless valid_modes.include?(mode.to_s)
|
|
113
|
+
raise ArgumentError, "Invalid mode: #{mode}. Must be one of: #{valid_modes.join(", ")}"
|
|
114
|
+
end
|
|
115
|
+
mode.to_s
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def default_metadata
|
|
119
|
+
{
|
|
120
|
+
hostname: Socket.gethostname,
|
|
121
|
+
project_dir: Dir.pwd,
|
|
122
|
+
ruby_version: RUBY_VERSION
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def compute_checksum
|
|
127
|
+
# Compute SHA256 of checkpoint data (excluding checksum field)
|
|
128
|
+
# Use canonical JSON with sorted keys for deterministic hashing
|
|
129
|
+
data = {
|
|
130
|
+
checkpoint_id: @checkpoint_id,
|
|
131
|
+
created_at: @created_at.utc.iso8601(6), # Preserve microsecond precision
|
|
132
|
+
aidp_version: @aidp_version,
|
|
133
|
+
mode: @mode,
|
|
134
|
+
watch_state: @watch_state,
|
|
135
|
+
metadata: @metadata
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
canonical_json = JSON.generate(sort_keys(data))
|
|
139
|
+
Digest::SHA256.hexdigest(canonical_json)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def sort_keys(obj)
|
|
143
|
+
case obj
|
|
144
|
+
when Hash
|
|
145
|
+
# Convert all keys to strings and sort for consistent ordering
|
|
146
|
+
obj.transform_keys(&:to_s).sort.to_h { |k, v| [k, sort_keys(v)] }
|
|
147
|
+
when Array
|
|
148
|
+
obj.map { |v| sort_keys(v) }
|
|
149
|
+
else
|
|
150
|
+
obj
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
class << self
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def capture_worktree_context
|
|
158
|
+
# Check if we're in a git repository
|
|
159
|
+
_stdout, _stderr, status = Open3.capture3("git", "rev-parse", "--git-dir")
|
|
160
|
+
return {} unless status.success?
|
|
161
|
+
|
|
162
|
+
branch, = Open3.capture3("git", "rev-parse", "--abbrev-ref", "HEAD")
|
|
163
|
+
commit_sha, = Open3.capture3("git", "rev-parse", "HEAD")
|
|
164
|
+
remote_url, = Open3.capture3("git", "config", "--get", "remote.origin.url")
|
|
165
|
+
|
|
166
|
+
{
|
|
167
|
+
branch: branch.strip,
|
|
168
|
+
commit_sha: commit_sha.strip,
|
|
169
|
+
remote_url: remote_url.strip
|
|
170
|
+
}
|
|
171
|
+
rescue => e
|
|
172
|
+
Aidp.log_debug("checkpoint", "worktree_context_unavailable", error: e.message)
|
|
173
|
+
{}
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "checkpoint"
|
|
6
|
+
require_relative "../safe_directory"
|
|
7
|
+
|
|
8
|
+
module Aidp
|
|
9
|
+
module AutoUpdate
|
|
10
|
+
# Repository for persisting and restoring checkpoint state
|
|
11
|
+
class CheckpointStore
|
|
12
|
+
include Aidp::SafeDirectory
|
|
13
|
+
|
|
14
|
+
attr_reader :checkpoint_dir
|
|
15
|
+
|
|
16
|
+
def initialize(project_dir: Dir.pwd)
|
|
17
|
+
@project_dir = project_dir
|
|
18
|
+
original_dir = File.join(project_dir, ".aidp", "checkpoints")
|
|
19
|
+
@checkpoint_dir = safe_mkdir_p(original_dir, component_name: "CheckpointStore")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Save checkpoint atomically
|
|
23
|
+
# @param checkpoint [Checkpoint] State to persist
|
|
24
|
+
# @return [Boolean] Success status
|
|
25
|
+
def save_checkpoint(checkpoint)
|
|
26
|
+
Aidp.log_info("checkpoint_store", "saving_checkpoint",
|
|
27
|
+
id: checkpoint.checkpoint_id,
|
|
28
|
+
mode: checkpoint.mode)
|
|
29
|
+
|
|
30
|
+
unless checkpoint.valid?
|
|
31
|
+
Aidp.log_error("checkpoint_store", "invalid_checkpoint",
|
|
32
|
+
id: checkpoint.checkpoint_id,
|
|
33
|
+
error: "Checksum validation failed")
|
|
34
|
+
return false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Write to temp file first
|
|
38
|
+
temp_file = "#{checkpoint_path(checkpoint.checkpoint_id)}.tmp"
|
|
39
|
+
File.write(temp_file, JSON.pretty_generate(checkpoint.to_h))
|
|
40
|
+
|
|
41
|
+
# Atomic rename
|
|
42
|
+
File.rename(temp_file, checkpoint_path(checkpoint.checkpoint_id))
|
|
43
|
+
|
|
44
|
+
Aidp.log_info("checkpoint_store", "checkpoint_saved",
|
|
45
|
+
id: checkpoint.checkpoint_id,
|
|
46
|
+
path: checkpoint_path(checkpoint.checkpoint_id))
|
|
47
|
+
true
|
|
48
|
+
rescue => e
|
|
49
|
+
Aidp.log_error("checkpoint_store", "save_failed",
|
|
50
|
+
id: checkpoint.checkpoint_id,
|
|
51
|
+
error: e.message)
|
|
52
|
+
File.delete(temp_file) if temp_file && File.exist?(temp_file)
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Find most recent checkpoint for restoration
|
|
57
|
+
# @return [Checkpoint, nil] Most recent checkpoint or nil
|
|
58
|
+
def latest_checkpoint
|
|
59
|
+
checkpoints = list_checkpoints
|
|
60
|
+
|
|
61
|
+
if checkpoints.empty?
|
|
62
|
+
Aidp.log_debug("checkpoint_store", "no_checkpoints_found")
|
|
63
|
+
return nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Sort by created_at descending
|
|
67
|
+
latest = checkpoints.max_by { |cp| cp.created_at }
|
|
68
|
+
|
|
69
|
+
Aidp.log_info("checkpoint_store", "found_latest_checkpoint",
|
|
70
|
+
id: latest.checkpoint_id,
|
|
71
|
+
created_at: latest.created_at.iso8601)
|
|
72
|
+
|
|
73
|
+
latest
|
|
74
|
+
rescue => e
|
|
75
|
+
Aidp.log_error("checkpoint_store", "latest_checkpoint_failed",
|
|
76
|
+
error: e.message)
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# List all checkpoints
|
|
81
|
+
# @return [Array<Checkpoint>] All available checkpoints
|
|
82
|
+
def list_checkpoints
|
|
83
|
+
checkpoint_files = Dir.glob(File.join(@checkpoint_dir, "*.json"))
|
|
84
|
+
|
|
85
|
+
checkpoints = checkpoint_files.filter_map do |file|
|
|
86
|
+
load_checkpoint_file(file)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
Aidp.log_debug("checkpoint_store", "listed_checkpoints",
|
|
90
|
+
count: checkpoints.size)
|
|
91
|
+
|
|
92
|
+
checkpoints
|
|
93
|
+
rescue => e
|
|
94
|
+
Aidp.log_error("checkpoint_store", "list_failed", error: e.message)
|
|
95
|
+
[]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Delete checkpoint after successful restoration
|
|
99
|
+
# @param checkpoint_id [String] Checkpoint UUID
|
|
100
|
+
# @return [Boolean] Success status
|
|
101
|
+
def delete_checkpoint(checkpoint_id)
|
|
102
|
+
path = checkpoint_path(checkpoint_id)
|
|
103
|
+
|
|
104
|
+
unless File.exist?(path)
|
|
105
|
+
Aidp.log_warn("checkpoint_store", "checkpoint_not_found",
|
|
106
|
+
id: checkpoint_id,
|
|
107
|
+
path: path)
|
|
108
|
+
return false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
File.delete(path)
|
|
112
|
+
|
|
113
|
+
Aidp.log_info("checkpoint_store", "checkpoint_deleted",
|
|
114
|
+
id: checkpoint_id)
|
|
115
|
+
true
|
|
116
|
+
rescue => e
|
|
117
|
+
Aidp.log_error("checkpoint_store", "delete_failed",
|
|
118
|
+
id: checkpoint_id,
|
|
119
|
+
error: e.message)
|
|
120
|
+
false
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Clean up old checkpoints (retention policy)
|
|
124
|
+
# @param max_age_days [Integer] Maximum age to retain
|
|
125
|
+
# @return [Integer] Number of checkpoints deleted
|
|
126
|
+
def cleanup_old_checkpoints(max_age_days: 7)
|
|
127
|
+
Aidp.log_info("checkpoint_store", "cleaning_old_checkpoints",
|
|
128
|
+
max_age_days: max_age_days)
|
|
129
|
+
|
|
130
|
+
cutoff_time = Time.now - (max_age_days * 24 * 60 * 60)
|
|
131
|
+
deleted_count = 0
|
|
132
|
+
|
|
133
|
+
list_checkpoints.each do |checkpoint|
|
|
134
|
+
if checkpoint.created_at < cutoff_time
|
|
135
|
+
if delete_checkpoint(checkpoint.checkpoint_id)
|
|
136
|
+
deleted_count += 1
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
Aidp.log_info("checkpoint_store", "cleanup_complete",
|
|
142
|
+
deleted: deleted_count,
|
|
143
|
+
max_age_days: max_age_days)
|
|
144
|
+
|
|
145
|
+
deleted_count
|
|
146
|
+
rescue => e
|
|
147
|
+
Aidp.log_error("checkpoint_store", "cleanup_failed",
|
|
148
|
+
error: e.message)
|
|
149
|
+
0
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def checkpoint_path(checkpoint_id)
|
|
155
|
+
File.join(@checkpoint_dir, "#{checkpoint_id}.json")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def load_checkpoint_file(file_path)
|
|
159
|
+
data = JSON.parse(File.read(file_path))
|
|
160
|
+
checkpoint = Checkpoint.from_h(data)
|
|
161
|
+
|
|
162
|
+
unless checkpoint.valid?
|
|
163
|
+
Aidp.log_warn("checkpoint_store", "invalid_checkpoint_checksum",
|
|
164
|
+
file: file_path)
|
|
165
|
+
return nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
checkpoint
|
|
169
|
+
rescue JSON::ParserError => e
|
|
170
|
+
Aidp.log_warn("checkpoint_store", "invalid_checkpoint_json",
|
|
171
|
+
file: file_path,
|
|
172
|
+
error: e.message)
|
|
173
|
+
nil
|
|
174
|
+
rescue => e
|
|
175
|
+
Aidp.log_warn("checkpoint_store", "checkpoint_load_failed",
|
|
176
|
+
file: file_path,
|
|
177
|
+
error: e.message)
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "version_detector"
|
|
4
|
+
require_relative "checkpoint_store"
|
|
5
|
+
require_relative "update_logger"
|
|
6
|
+
require_relative "failure_tracker"
|
|
7
|
+
require_relative "update_policy"
|
|
8
|
+
require_relative "errors"
|
|
9
|
+
|
|
10
|
+
module Aidp
|
|
11
|
+
module AutoUpdate
|
|
12
|
+
# Facade for orchestrating the complete auto-update workflow
|
|
13
|
+
class Coordinator
|
|
14
|
+
attr_reader :policy, :version_detector, :checkpoint_store, :update_logger, :failure_tracker
|
|
15
|
+
|
|
16
|
+
def initialize(
|
|
17
|
+
policy:,
|
|
18
|
+
version_detector: nil,
|
|
19
|
+
checkpoint_store: nil,
|
|
20
|
+
update_logger: nil,
|
|
21
|
+
failure_tracker: nil,
|
|
22
|
+
project_dir: Dir.pwd
|
|
23
|
+
)
|
|
24
|
+
@policy = policy
|
|
25
|
+
@project_dir = project_dir
|
|
26
|
+
|
|
27
|
+
# Use provided instances or create defaults
|
|
28
|
+
@version_detector = version_detector || VersionDetector.new(policy: policy)
|
|
29
|
+
@checkpoint_store = checkpoint_store || CheckpointStore.new(project_dir: project_dir)
|
|
30
|
+
@update_logger = update_logger || UpdateLogger.new(project_dir: project_dir)
|
|
31
|
+
@failure_tracker = failure_tracker || FailureTracker.new(
|
|
32
|
+
project_dir: project_dir,
|
|
33
|
+
max_failures: policy.max_consecutive_failures
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Create coordinator from configuration
|
|
38
|
+
# @param config [Hash] Auto-update configuration from aidp.yml
|
|
39
|
+
# @param project_dir [String] Project root directory
|
|
40
|
+
# @return [Coordinator]
|
|
41
|
+
def self.from_config(config, project_dir: Dir.pwd)
|
|
42
|
+
policy = UpdatePolicy.from_config(config)
|
|
43
|
+
new(policy: policy, project_dir: project_dir)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check if update is available and allowed
|
|
47
|
+
# @return [UpdateCheck] Update check result
|
|
48
|
+
def check_for_update
|
|
49
|
+
return UpdateCheck.unavailable unless @policy.enabled
|
|
50
|
+
|
|
51
|
+
update_check = @version_detector.check_for_update
|
|
52
|
+
@update_logger.log_check(update_check)
|
|
53
|
+
update_check
|
|
54
|
+
rescue => e
|
|
55
|
+
Aidp.log_error("auto_update_coordinator", "check_failed",
|
|
56
|
+
error: e.message)
|
|
57
|
+
UpdateCheck.failed(e.message)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Initiate update process (checkpoint + exit with code 75)
|
|
61
|
+
# @param current_state [Hash] Current application state (from Watch::Runner)
|
|
62
|
+
# @return [void] (exits process with code 75)
|
|
63
|
+
# @raise [UpdateError] If updates are disabled or preconditions not met
|
|
64
|
+
# @raise [UpdateLoopError] If too many consecutive failures
|
|
65
|
+
def initiate_update(current_state)
|
|
66
|
+
raise UpdateError, "Updates disabled by configuration" unless @policy.enabled
|
|
67
|
+
|
|
68
|
+
# Check for restart loops
|
|
69
|
+
if @failure_tracker.too_many_failures?
|
|
70
|
+
@update_logger.log_restart_loop(@failure_tracker.failure_count)
|
|
71
|
+
raise UpdateLoopError, "Too many consecutive update failures (#{@failure_tracker.failure_count}/#{@policy.max_consecutive_failures})"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Verify supervisor is configured
|
|
75
|
+
unless @policy.supervised?
|
|
76
|
+
raise UpdateError, "No supervisor configured. Set auto_update.supervisor in aidp.yml"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get latest version to record in checkpoint
|
|
80
|
+
update_check = check_for_update
|
|
81
|
+
|
|
82
|
+
unless update_check.should_update?
|
|
83
|
+
Aidp.log_info("auto_update_coordinator", "no_update_needed",
|
|
84
|
+
reason: update_check.policy_reason)
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Create checkpoint from current state
|
|
89
|
+
checkpoint = build_checkpoint(current_state, update_check.available_version)
|
|
90
|
+
|
|
91
|
+
# Save checkpoint
|
|
92
|
+
unless @checkpoint_store.save_checkpoint(checkpoint)
|
|
93
|
+
raise UpdateError, "Failed to save checkpoint"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Log update initiation
|
|
97
|
+
@update_logger.log_update_initiated(checkpoint, target_version: update_check.available_version)
|
|
98
|
+
|
|
99
|
+
Aidp.log_info("auto_update_coordinator", "exiting_for_update",
|
|
100
|
+
from_version: update_check.current_version,
|
|
101
|
+
to_version: update_check.available_version,
|
|
102
|
+
checkpoint_id: checkpoint.checkpoint_id)
|
|
103
|
+
|
|
104
|
+
# Exit with special code 75 to signal supervisor to update
|
|
105
|
+
exit(75)
|
|
106
|
+
rescue UpdateError, UpdateLoopError
|
|
107
|
+
# Re-raise domain errors
|
|
108
|
+
raise
|
|
109
|
+
rescue => e
|
|
110
|
+
@failure_tracker.record_failure
|
|
111
|
+
@update_logger.log_failure(e.message)
|
|
112
|
+
Aidp.log_error("auto_update_coordinator", "initiate_failed",
|
|
113
|
+
error: e.message)
|
|
114
|
+
raise UpdateError, "Update initiation failed: #{e.message}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Restore from checkpoint after update
|
|
118
|
+
# @return [Checkpoint, nil] Restored checkpoint or nil
|
|
119
|
+
def restore_from_checkpoint
|
|
120
|
+
checkpoint = @checkpoint_store.latest_checkpoint
|
|
121
|
+
return nil unless checkpoint
|
|
122
|
+
|
|
123
|
+
Aidp.log_info("auto_update_coordinator", "restoring_checkpoint",
|
|
124
|
+
id: checkpoint.checkpoint_id,
|
|
125
|
+
created_at: checkpoint.created_at.iso8601)
|
|
126
|
+
|
|
127
|
+
# Validate checkpoint
|
|
128
|
+
unless checkpoint.valid?
|
|
129
|
+
Aidp.log_error("auto_update_coordinator", "invalid_checkpoint",
|
|
130
|
+
id: checkpoint.checkpoint_id,
|
|
131
|
+
reason: "Checksum validation failed")
|
|
132
|
+
@failure_tracker.record_failure
|
|
133
|
+
@update_logger.log_failure("Invalid checkpoint checksum", checkpoint_id: checkpoint.checkpoint_id)
|
|
134
|
+
return nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Check version compatibility
|
|
138
|
+
unless checkpoint.compatible_version?
|
|
139
|
+
Aidp.log_warn("auto_update_coordinator", "incompatible_version",
|
|
140
|
+
checkpoint_version: checkpoint.aidp_version,
|
|
141
|
+
current_version: Aidp::VERSION)
|
|
142
|
+
@failure_tracker.record_failure
|
|
143
|
+
@update_logger.log_failure(
|
|
144
|
+
"Incompatible version: checkpoint from #{checkpoint.aidp_version}, current #{Aidp::VERSION}",
|
|
145
|
+
checkpoint_id: checkpoint.checkpoint_id
|
|
146
|
+
)
|
|
147
|
+
return nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Log successful restoration
|
|
151
|
+
@update_logger.log_restore(checkpoint)
|
|
152
|
+
@update_logger.log_success(
|
|
153
|
+
from_version: checkpoint.aidp_version,
|
|
154
|
+
to_version: Aidp::VERSION
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Reset failure tracker on success
|
|
158
|
+
@failure_tracker.reset_on_success
|
|
159
|
+
|
|
160
|
+
# Delete checkpoint after successful restore
|
|
161
|
+
@checkpoint_store.delete_checkpoint(checkpoint.checkpoint_id)
|
|
162
|
+
|
|
163
|
+
checkpoint
|
|
164
|
+
rescue => e
|
|
165
|
+
@failure_tracker.record_failure
|
|
166
|
+
@update_logger.log_failure("Checkpoint restore failed: #{e.message}")
|
|
167
|
+
Aidp.log_error("auto_update_coordinator", "restore_failed",
|
|
168
|
+
error: e.message)
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Get status summary for CLI display
|
|
173
|
+
# @return [Hash] Status information
|
|
174
|
+
def status
|
|
175
|
+
update_check = check_for_update
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
enabled: @policy.enabled,
|
|
179
|
+
policy: @policy.policy,
|
|
180
|
+
supervisor: @policy.supervisor,
|
|
181
|
+
current_version: Aidp::VERSION,
|
|
182
|
+
available_version: update_check.available_version,
|
|
183
|
+
update_available: update_check.update_available,
|
|
184
|
+
update_allowed: update_check.update_allowed,
|
|
185
|
+
policy_reason: update_check.policy_reason,
|
|
186
|
+
failure_tracker: @failure_tracker.status,
|
|
187
|
+
recent_updates: @update_logger.recent_entries(limit: 5)
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
def build_checkpoint(current_state, target_version)
|
|
194
|
+
Checkpoint.new(
|
|
195
|
+
mode: current_state[:mode] || "watch",
|
|
196
|
+
watch_state: current_state[:watch_state]
|
|
197
|
+
).tap do |cp|
|
|
198
|
+
# Store target version in metadata for logging
|
|
199
|
+
cp.instance_variable_set(:@target_version, target_version)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|