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.
- checksums.yaml +4 -4
- data/README.md +72 -7
- data/lib/aidp/analyze/error_handler.rb +11 -0
- 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/execute/work_loop_runner.rb +225 -55
- data/lib/aidp/harness/config_loader.rb +20 -11
- data/lib/aidp/harness/config_schema.rb +80 -8
- data/lib/aidp/harness/configuration.rb +73 -2
- data/lib/aidp/harness/filter_strategy.rb +45 -0
- data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
- data/lib/aidp/harness/output_filter.rb +136 -0
- data/lib/aidp/harness/provider_factory.rb +2 -0
- data/lib/aidp/harness/provider_manager.rb +18 -3
- data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
- data/lib/aidp/harness/test_runner.rb +165 -27
- data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
- data/lib/aidp/logger.rb +35 -5
- data/lib/aidp/message_display.rb +56 -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/safe_directory.rb +10 -3
- 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/storage/csv_storage.rb +9 -3
- data/lib/aidp/storage/file_manager.rb +8 -2
- data/lib/aidp/storage/json_storage.rb +9 -3
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +106 -17
- data/lib/aidp/watch/change_request_processor.rb +659 -0
- data/lib/aidp/watch/ci_fix_processor.rb +448 -0
- data/lib/aidp/watch/plan_processor.rb +81 -8
- data/lib/aidp/watch/repository_client.rb +465 -20
- 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 +222 -0
- data/lib/aidp/watch/state_store.rb +99 -1
- data/lib/aidp/workstream_executor.rb +5 -2
- data/lib/aidp.rb +5 -0
- data/templates/aidp.yml.example +53 -0
- metadata +25 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b4770edc850e936b3201ec532cdd369f7fd881ff79dd10e24407c6453b8cb22
|
|
4
|
+
data.tar.gz: d7d2640aa63d2c25afbf795bbf77d48741bdd99d6a074765fa5595f3982b96e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|