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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2ec07f1c36212b7ace8e467e098fc3bcd917dc0738e27125bbddcef9bbcc30e
4
- data.tar.gz: edbc40f6c50b581729d185186eae3510e8c23885f12655c46868117af361f31a
3
+ metadata.gz: 348238e998f8d75ef9f23fcb5a8f4517b6fde0dd9612a4520583b03ab692cc23
4
+ data.tar.gz: ed8db8dbd0459211e65b76784b6074b65f2689cdb76f7b7c74bcb029d4c9f74d
5
5
  SHA512:
6
- metadata.gz: f6472e33b88cf45f0c0abc5131dd608ba20d73480074552275d6d886222357c435023845fa8762b7f527268ba34fc2d3841d504309c8c068adbc38d6ac41f12b
7
- data.tar.gz: 4283ba15ee02985603adbdba0be476adec35efc9fa6730a01beef1eac50d068647f3560aa03d75fcdb24ffc102e742bc1c3d9ff53fe95662a20241c711b54ae7
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