aidp 0.24.0 → 0.26.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +72 -7
  3. data/lib/aidp/analyze/error_handler.rb +11 -0
  4. data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
  5. data/lib/aidp/auto_update/checkpoint.rb +178 -0
  6. data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
  7. data/lib/aidp/auto_update/coordinator.rb +204 -0
  8. data/lib/aidp/auto_update/errors.rb +17 -0
  9. data/lib/aidp/auto_update/failure_tracker.rb +162 -0
  10. data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
  11. data/lib/aidp/auto_update/update_check.rb +106 -0
  12. data/lib/aidp/auto_update/update_logger.rb +143 -0
  13. data/lib/aidp/auto_update/update_policy.rb +109 -0
  14. data/lib/aidp/auto_update/version_detector.rb +144 -0
  15. data/lib/aidp/auto_update.rb +52 -0
  16. data/lib/aidp/cli.rb +165 -1
  17. data/lib/aidp/execute/work_loop_runner.rb +225 -55
  18. data/lib/aidp/harness/config_loader.rb +20 -11
  19. data/lib/aidp/harness/config_schema.rb +80 -8
  20. data/lib/aidp/harness/configuration.rb +73 -2
  21. data/lib/aidp/harness/filter_strategy.rb +45 -0
  22. data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
  23. data/lib/aidp/harness/output_filter.rb +136 -0
  24. data/lib/aidp/harness/provider_factory.rb +2 -0
  25. data/lib/aidp/harness/provider_manager.rb +18 -3
  26. data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
  27. data/lib/aidp/harness/test_runner.rb +165 -27
  28. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
  29. data/lib/aidp/logger.rb +35 -5
  30. data/lib/aidp/message_display.rb +56 -2
  31. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
  32. data/lib/aidp/provider_manager.rb +2 -0
  33. data/lib/aidp/providers/kilocode.rb +202 -0
  34. data/lib/aidp/safe_directory.rb +10 -3
  35. data/lib/aidp/setup/provider_registry.rb +15 -0
  36. data/lib/aidp/setup/wizard.rb +12 -4
  37. data/lib/aidp/skills/composer.rb +4 -0
  38. data/lib/aidp/skills/loader.rb +3 -1
  39. data/lib/aidp/storage/csv_storage.rb +9 -3
  40. data/lib/aidp/storage/file_manager.rb +8 -2
  41. data/lib/aidp/storage/json_storage.rb +9 -3
  42. data/lib/aidp/version.rb +1 -1
  43. data/lib/aidp/watch/build_processor.rb +106 -17
  44. data/lib/aidp/watch/change_request_processor.rb +659 -0
  45. data/lib/aidp/watch/ci_fix_processor.rb +448 -0
  46. data/lib/aidp/watch/plan_processor.rb +81 -8
  47. data/lib/aidp/watch/repository_client.rb +465 -20
  48. data/lib/aidp/watch/review_processor.rb +266 -0
  49. data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
  50. data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
  51. data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
  52. data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
  53. data/lib/aidp/watch/runner.rb +222 -0
  54. data/lib/aidp/watch/state_store.rb +99 -1
  55. data/lib/aidp/workstream_executor.rb +5 -2
  56. data/lib/aidp.rb +5 -0
  57. data/templates/aidp.yml.example +53 -0
  58. metadata +25 -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: 0b4770edc850e936b3201ec532cdd369f7fd881ff79dd10e24407c6453b8cb22
4
+ data.tar.gz: d7d2640aa63d2c25afbf795bbf77d48741bdd99d6a074765fa5595f3982b96e6
5
5
  SHA512:
6
- metadata.gz: f6472e33b88cf45f0c0abc5131dd608ba20d73480074552275d6d886222357c435023845fa8762b7f527268ba34fc2d3841d504309c8c068adbc38d6ac41f12b
7
- data.tar.gz: 4283ba15ee02985603adbdba0be476adec35efc9fa6730a01beef1eac50d068647f3560aa03d75fcdb24ffc102e742bc1c3d9ff53fe95662a20241c711b54ae7
6
+ metadata.gz: 1841e0b35478d65d382d13dd138955c30655002b91c2a83d96ec2c83925eec4f954892aa710ded867b50554dbc9653bddfad5c5aa915890e674bb49afed347bc
7
+ data.tar.gz: 1fc1443566c4e01e0c6ff25f1ae62c890ddcd5373257265ee7119ea8db359c20282cf646d61b955204e4dc538ceb9c6313725cb4fed21cda231b97321cc2c030
data/README.md CHANGED
@@ -218,7 +218,9 @@ aidp watch owner/repo --once
218
218
 
219
219
  **Label Workflow:**
220
220
 
221
- AIDP uses a smart label-based workflow to manage the lifecycle of automated issue resolution:
221
+ AIDP uses a smart label-based workflow to manage both issues and pull requests:
222
+
223
+ #### Issue Workflow (Plan & Build)
222
224
 
223
225
  1. **Planning Phase** (`aidp-plan` label):
224
226
  - Add this label to an issue to trigger plan generation
@@ -247,6 +249,32 @@ AIDP uses a smart label-based workflow to manage the lifecycle of automated issu
247
249
  - Posts completion comment with summary
248
250
  - Automatically removes the `aidp-build` label
249
251
 
252
+ #### Pull Request Workflow (Review, CI Fix, Change Requests)
253
+
254
+ <!-- markdownlint-disable-next-line MD029 -->
255
+ 1. **Code Review** (`aidp-review` label):
256
+ - Add this label to any PR to trigger automated code review
257
+ - AIDP analyzes code from three expert perspectives (Senior Developer, Security Specialist, Performance Analyst)
258
+ - Posts a comprehensive review comment with severity-categorized findings (High Priority, Major, Minor, Nit)
259
+ - Automatically removes the label after posting review
260
+ - No commits are made - review only
261
+
262
+ 2. **CI Fix** (`aidp-fix-ci` label):
263
+ - Add this label to a PR with failing CI checks
264
+ - AIDP analyzes CI failure logs and identifies root causes
265
+ - Automatically fixes issues like linting errors, simple test failures, and dependency problems
266
+ - Commits and pushes fixes to the PR branch
267
+ - Posts a summary of what was fixed
268
+ - Automatically removes the label after completion
269
+
270
+ 3. **Change Requests** (`aidp-request-changes` label):
271
+ - Comment on your own PR describing desired changes, then add this label
272
+ - AIDP implements the requested changes on the PR branch
273
+ - Runs tests/linters and commits changes
274
+ - **If clarification needed**: Replaces label with `aidp-needs-input` and posts questions
275
+ - User responds to questions and re-applies the label to continue
276
+ - Automatically removes the label after completion
277
+
250
278
  **Customizable Labels:**
251
279
 
252
280
  All label names are configurable to match your repository's existing label scheme. Configure via the interactive wizard or manually in `aidp.yml`:
@@ -255,10 +283,16 @@ All label names are configurable to match your repository's existing label schem
255
283
  # .aidp/aidp.yml
256
284
  watch:
257
285
  labels:
258
- plan_trigger: aidp-plan # Label to trigger plan generation
259
- needs_input: aidp-needs-input # Label when plan needs user input
260
- ready_to_build: aidp-ready # Label when plan is ready to build
261
- build_trigger: aidp-build # Label to trigger implementation
286
+ # Issue-based automation
287
+ plan_trigger: aidp-plan # Trigger plan generation
288
+ needs_input: aidp-needs-input # Needs user input/clarification
289
+ ready_to_build: aidp-ready # Plan ready for implementation
290
+ build_trigger: aidp-build # Trigger implementation
291
+
292
+ # PR-based automation
293
+ review_trigger: aidp-review # Trigger code review
294
+ ci_fix_trigger: aidp-fix-ci # Trigger CI auto-fix
295
+ change_request_trigger: aidp-request-changes # Trigger PR change implementation
262
296
  ```
263
297
 
264
298
  Run `aidp config --interactive` and enable watch mode to configure labels interactively.
@@ -294,7 +328,12 @@ AIDP can automatically request clarification when it needs more information duri
294
328
 
295
329
  This ensures AIDP never gets stuck - if it needs more information, it will ask for it rather than making incorrect assumptions or failing silently.
296
330
 
297
- See [Watch Mode Guide](docs/FULLY_AUTOMATIC_MODE.md) and [Watch Mode Safety](docs/WATCH_MODE_SAFETY.md) for complete documentation.
331
+ **Additional Documentation:**
332
+
333
+ - [Watch Mode Guide](docs/FULLY_AUTOMATIC_MODE.md) - Complete guide to watch mode setup and operation
334
+ - [Watch Mode Safety](docs/WATCH_MODE_SAFETY.md) - Security features and best practices
335
+ - [PR Automation Guide](docs/PR_AUTOMATION.md) - Detailed guide for code review, CI fixes, and PR changes
336
+ - [PR Change Requests](docs/PR_CHANGE_REQUESTS.md) - Comprehensive documentation for automated PR modifications
298
337
 
299
338
  ## Command Reference
300
339
 
@@ -406,6 +445,7 @@ AIDP intelligently manages multiple providers with automatic switching:
406
445
  - **Cursor CLI** - IDE-integrated provider for code-specific tasks
407
446
  - **Gemini CLI** - Google's Gemini command-line interface for general tasks
408
447
  - **GitHub Copilot CLI** - GitHub's AI pair programmer command-line interface
448
+ - **Kilocode** - Modern AI coding assistant with autonomous mode support
409
449
  - **OpenCode** - Alternative open-source code generation provider
410
450
 
411
451
  The system automatically switches providers when:
@@ -456,12 +496,37 @@ providers:
456
496
  type: "subscription"
457
497
  ```
458
498
 
499
+ ### Provider Installation
500
+
501
+ Each provider requires its CLI tool to be installed:
502
+
503
+ ```bash
504
+ # Cursor CLI
505
+ npm install -g @cursor/cli
506
+
507
+ # Kilocode CLI
508
+ npm install -g @kilocode/cli
509
+
510
+ # OpenCode CLI
511
+ npm install -g @opencode/cli
512
+
513
+ # GitHub Copilot CLI (requires GitHub account)
514
+ gh extension install github/gh-copilot
515
+ ```
516
+
459
517
  ### Environment Variables
460
518
 
461
519
  ```bash
462
- # Set API keys
520
+ # Set API keys for usage-based providers
463
521
  export AIDP_CLAUDE_API_KEY="your-claude-api-key"
464
522
  export AIDP_GEMINI_API_KEY="your-gemini-api-key"
523
+
524
+ # Kilocode authentication (get token from kilocode.ai profile)
525
+ export KILOCODE_TOKEN="your-kilocode-api-token"
526
+
527
+ # Optional: Configure provider-specific settings
528
+ export KILOCODE_MODEL="your-preferred-model"
529
+ export AIDP_KILOCODE_TIMEOUT="600" # Custom timeout in seconds
465
530
  ```
466
531
 
467
532
  ## Tree-sitter Static Analysis
@@ -99,6 +99,13 @@ module Aidp
99
99
  private
100
100
 
101
101
  def setup_logger(log_file, verbose)
102
+ # Suppress logger output in test/CI environments
103
+ if suppress_error_logs?
104
+ logger = ::Logger.new(IO::NULL)
105
+ logger.level = ::Logger::FATAL
106
+ return logger
107
+ end
108
+
102
109
  output_stream = log_file || @output || $stdout
103
110
  logger = ::Logger.new(output_stream)
104
111
  logger.level = verbose ? ::Logger::DEBUG : ::Logger::INFO
@@ -108,6 +115,10 @@ module Aidp
108
115
  logger
109
116
  end
110
117
 
118
+ def suppress_error_logs?
119
+ ENV["RSPEC_RUNNING"] || ENV["CI"] || ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
120
+ end
121
+
111
122
  def setup_recovery_strategies
112
123
  strategies = {
113
124
  Errno::ENOENT => :skip_step_with_warning,
@@ -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