aidp 0.31.0 → 0.32.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/lib/aidp/execute/work_loop_runner.rb +70 -0
- data/lib/aidp/harness/provider_manager.rb +38 -0
- data/lib/aidp/harness/provider_metrics.rb +5 -3
- data/lib/aidp/pr_worktree_manager.rb +582 -0
- data/lib/aidp/util.rb +11 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/worktree_branch_manager.rb +70 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7a02d5fb9cd901a6a74a2b402c5b9d70ea07f47727ddbd388993e9ede64c96b
|
|
4
|
+
data.tar.gz: fc92260f2b244295b98b5e2e2cdedf4872a18f94f5c3b8bf0031b6a4fc1f6554
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 673505d4036ad47cff7ff1b534e1798e893dcf7fc77f086ee1b6f46ba90b48de4b72333cf63b15db1fd0b16b2b0060c324fe6a1bdf371ae7ec796ce606b454e7
|
|
7
|
+
data.tar.gz: f99df6bea4120ec723a907e4f4a1cc15dc1c5c52abf3c4753bc5cdd24e6288a5207dca6ae6dc85a6afab442dc2b845de7d7794731f1bd27caeaf22c9e46f656f
|
|
@@ -7,6 +7,7 @@ require_relative "guard_policy"
|
|
|
7
7
|
require_relative "work_loop_unit_scheduler"
|
|
8
8
|
require_relative "deterministic_unit"
|
|
9
9
|
require_relative "agent_signal_parser"
|
|
10
|
+
require_relative "steps"
|
|
10
11
|
require_relative "../harness/test_runner"
|
|
11
12
|
require_relative "../errors"
|
|
12
13
|
|
|
@@ -81,6 +82,7 @@ module Aidp
|
|
|
81
82
|
|
|
82
83
|
display_message("🔄 Starting hybrid work loop for step: #{step_name}", type: :info)
|
|
83
84
|
display_message(" Flow: Deterministic ↔ Agentic with fix-forward core", type: :info)
|
|
85
|
+
display_work_context(step_name, context)
|
|
84
86
|
|
|
85
87
|
display_guard_policy_status
|
|
86
88
|
display_pending_tasks
|
|
@@ -1286,6 +1288,74 @@ module Aidp
|
|
|
1286
1288
|
display_message("")
|
|
1287
1289
|
end
|
|
1288
1290
|
|
|
1291
|
+
# Show watch-mode context (issue/PR, step position) to improve situational awareness
|
|
1292
|
+
def display_work_context(step_name, context)
|
|
1293
|
+
parts = work_context_parts(step_name, context)
|
|
1294
|
+
return if parts.empty?
|
|
1295
|
+
|
|
1296
|
+
Aidp.log_debug("work_loop", "work_context", step: step_name, parts: parts)
|
|
1297
|
+
display_message(" 📡 Context: #{parts.join(" | ")}", type: :info)
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
def work_context_parts(step_name, context)
|
|
1301
|
+
ctx = (@options || {}).merge(context || {})
|
|
1302
|
+
parts = []
|
|
1303
|
+
|
|
1304
|
+
if (step_label = step_position_label(step_name, ctx))
|
|
1305
|
+
parts << step_label
|
|
1306
|
+
end
|
|
1307
|
+
|
|
1308
|
+
if (issue_label = issue_context_label(ctx))
|
|
1309
|
+
parts << issue_label
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1312
|
+
if (pr_label = pr_context_label(ctx))
|
|
1313
|
+
parts << pr_label
|
|
1314
|
+
end
|
|
1315
|
+
|
|
1316
|
+
parts << "Watch mode" if ctx[:workflow_type].to_s == "watch_mode"
|
|
1317
|
+
|
|
1318
|
+
parts.compact
|
|
1319
|
+
end
|
|
1320
|
+
|
|
1321
|
+
def step_position_label(step_name, context)
|
|
1322
|
+
steps = Array(context[:selected_steps]).map(&:to_s)
|
|
1323
|
+
steps = Aidp::Execute::Steps::SPEC.keys if steps.empty?
|
|
1324
|
+
steps = [step_name] if steps.empty?
|
|
1325
|
+
steps << step_name unless steps.include?(step_name)
|
|
1326
|
+
|
|
1327
|
+
index = steps.index(step_name)
|
|
1328
|
+
return nil unless index
|
|
1329
|
+
|
|
1330
|
+
"Step #{index + 1}/#{steps.size} (#{step_name})"
|
|
1331
|
+
end
|
|
1332
|
+
|
|
1333
|
+
def issue_context_label(context)
|
|
1334
|
+
issue_number = context[:issue_number] ||
|
|
1335
|
+
context.dig(:issue, :number) ||
|
|
1336
|
+
extract_number_from_url(context[:issue_url] || context.dig(:issue, :url) || context.dig(:user_input, "Issue URL"), /issues\/(\d+)/)
|
|
1337
|
+
|
|
1338
|
+
return nil unless issue_number
|
|
1339
|
+
|
|
1340
|
+
"Issue ##{issue_number}"
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1343
|
+
def pr_context_label(context)
|
|
1344
|
+
pr_number = context[:pr_number] ||
|
|
1345
|
+
context.dig(:pull_request, :number) ||
|
|
1346
|
+
extract_number_from_url(context[:pr_url] || context.dig(:pull_request, :url) || context.dig(:user_input, "PR URL") || context.dig(:user_input, "Pull Request URL"), /pull\/(\d+)/)
|
|
1347
|
+
|
|
1348
|
+
return nil unless pr_number
|
|
1349
|
+
|
|
1350
|
+
"PR ##{pr_number}"
|
|
1351
|
+
end
|
|
1352
|
+
|
|
1353
|
+
def extract_number_from_url(url, pattern)
|
|
1354
|
+
return nil unless url
|
|
1355
|
+
match = url.to_s.match(pattern)
|
|
1356
|
+
match && match[1]
|
|
1357
|
+
end
|
|
1358
|
+
|
|
1289
1359
|
# Append task completion requirement to PROMPT.md
|
|
1290
1360
|
def append_task_requirement_to_prompt(message)
|
|
1291
1361
|
task_requirement = []
|
|
@@ -38,6 +38,7 @@ module Aidp
|
|
|
38
38
|
@model_fallback_chains = {}
|
|
39
39
|
@model_switching_enabled = true
|
|
40
40
|
@model_weights = {}
|
|
41
|
+
@model_denylist = Hash.new { |h, k| h[k] = [] }
|
|
41
42
|
@unavailable_cache = {}
|
|
42
43
|
@binary_check_cache = {}
|
|
43
44
|
@binary_check_ttl = 300 # seconds
|
|
@@ -387,10 +388,30 @@ module Aidp
|
|
|
387
388
|
# Check if model is configured for provider
|
|
388
389
|
return false unless model_configured?(provider_name, model_name)
|
|
389
390
|
|
|
391
|
+
# Skip models that were explicitly denied (e.g., unsupported by provider)
|
|
392
|
+
return false if model_denied?(provider_name, model_name)
|
|
393
|
+
|
|
390
394
|
# Check if model is not rate limited
|
|
391
395
|
!is_model_rate_limited?(provider_name, model_name)
|
|
392
396
|
end
|
|
393
397
|
|
|
398
|
+
# Check if a model has been denylisted for a provider
|
|
399
|
+
def model_denied?(provider_name, model_name)
|
|
400
|
+
@model_denylist[provider_name]&.include?(model_name)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Add a model to the denylist for a provider (e.g., unsupported model errors)
|
|
404
|
+
def deny_model(provider_name, model_name, error: nil)
|
|
405
|
+
return if provider_name.nil? || model_name.nil?
|
|
406
|
+
return if model_denied?(provider_name, model_name)
|
|
407
|
+
|
|
408
|
+
@model_denylist[provider_name] << model_name
|
|
409
|
+
Aidp.log_debug("provider_manager", "model_denylisted",
|
|
410
|
+
provider: provider_name,
|
|
411
|
+
model: model_name,
|
|
412
|
+
error: error&.message)
|
|
413
|
+
end
|
|
414
|
+
|
|
394
415
|
# Check if model is configured for provider
|
|
395
416
|
def model_configured?(provider_name, model_name)
|
|
396
417
|
models = provider_models(provider_name)
|
|
@@ -1279,6 +1300,7 @@ module Aidp
|
|
|
1279
1300
|
@model_health.clear
|
|
1280
1301
|
@model_metrics.clear
|
|
1281
1302
|
@model_fallback_chains.clear
|
|
1303
|
+
@model_denylist.clear
|
|
1282
1304
|
@model_rate_limit_info&.clear
|
|
1283
1305
|
@model_history&.clear
|
|
1284
1306
|
initialize_fallback_chains
|
|
@@ -1436,6 +1458,10 @@ module Aidp
|
|
|
1436
1458
|
rescue => e
|
|
1437
1459
|
log_rescue(e, component: "provider_manager", action: "execute_with_provider", fallback: "error_result", provider: provider_type, model: model_name, prompt_length: prompt.length)
|
|
1438
1460
|
|
|
1461
|
+
if unsupported_model_error?(e, model_name)
|
|
1462
|
+
deny_model(provider_type, model_name, error: e)
|
|
1463
|
+
end
|
|
1464
|
+
|
|
1439
1465
|
# Detect rate limit / quota errors and attempt fallback
|
|
1440
1466
|
error_message = e.message.to_s.downcase
|
|
1441
1467
|
is_rate_limit = error_message.include?("rate limit") ||
|
|
@@ -1583,6 +1609,18 @@ module Aidp
|
|
|
1583
1609
|
((order[a] || 0) >= (order[b] || 0)) ? a : b
|
|
1584
1610
|
end
|
|
1585
1611
|
|
|
1612
|
+
# Detect unsupported/invalid model errors to avoid reusing the model
|
|
1613
|
+
def unsupported_model_error?(error, model_name)
|
|
1614
|
+
return false if model_name.nil?
|
|
1615
|
+
|
|
1616
|
+
message = error&.message.to_s.downcase
|
|
1617
|
+
return false if message.empty?
|
|
1618
|
+
|
|
1619
|
+
(message.include?("unsupported") && message.include?("model")) ||
|
|
1620
|
+
(message.include?("model") && message.include?("not supported")) ||
|
|
1621
|
+
message.include?("invalid model")
|
|
1622
|
+
end
|
|
1623
|
+
|
|
1586
1624
|
public
|
|
1587
1625
|
|
|
1588
1626
|
# Log provider switch
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require_relative "../rescue_logging"
|
|
6
|
+
require_relative "../util"
|
|
6
7
|
|
|
7
8
|
module Aidp
|
|
8
9
|
module Harness
|
|
@@ -14,9 +15,10 @@ module Aidp
|
|
|
14
15
|
attr_reader :project_dir, :metrics_file, :rate_limit_file
|
|
15
16
|
|
|
16
17
|
def initialize(project_dir)
|
|
17
|
-
|
|
18
|
-
@
|
|
19
|
-
@
|
|
18
|
+
# Store metrics at the repository root so different worktrees/modes share state
|
|
19
|
+
@project_dir = Aidp::Util.find_project_root(project_dir)
|
|
20
|
+
@metrics_file = File.join(@project_dir, ".aidp", "provider_metrics.yml")
|
|
21
|
+
@rate_limit_file = File.join(@project_dir, ".aidp", "provider_rate_limits.yml")
|
|
20
22
|
ensure_directory
|
|
21
23
|
end
|
|
22
24
|
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
# Manages worktrees specifically for Pull Request branches
|
|
7
|
+
class PrWorktreeManager
|
|
8
|
+
def initialize(base_repo_path: nil, project_dir: nil, worktree_registry_path: nil)
|
|
9
|
+
@base_repo_path = base_repo_path || project_dir || Dir.pwd
|
|
10
|
+
@project_dir = project_dir
|
|
11
|
+
@worktree_registry_path = worktree_registry_path || File.join(
|
|
12
|
+
project_dir || File.expand_path("~/.aidp"),
|
|
13
|
+
"pr_worktrees.json"
|
|
14
|
+
)
|
|
15
|
+
FileUtils.mkdir_p(File.dirname(@worktree_registry_path))
|
|
16
|
+
@worktrees = load_registry
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :worktree_registry_path
|
|
20
|
+
|
|
21
|
+
# Find an existing worktree for a given PR number or branch
|
|
22
|
+
def find_worktree(pr_number = nil, branch: nil)
|
|
23
|
+
Aidp.log_debug(
|
|
24
|
+
"pr_worktree_manager",
|
|
25
|
+
"finding_worktree",
|
|
26
|
+
pr_number: pr_number,
|
|
27
|
+
branch: branch
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Validate input
|
|
31
|
+
raise ArgumentError, "Must provide either pr_number or branch" if pr_number.nil? && branch.nil?
|
|
32
|
+
|
|
33
|
+
# First, check for exact PR match if PR number is provided
|
|
34
|
+
existing_worktree = pr_number ? @worktrees[pr_number.to_s] : nil
|
|
35
|
+
return validate_worktree_path(existing_worktree) if existing_worktree
|
|
36
|
+
|
|
37
|
+
# If no PR number, search by branch in all worktrees
|
|
38
|
+
matching_worktrees = @worktrees.values.select do |details|
|
|
39
|
+
# Check for exact branch match or remote branch match with advanced checks
|
|
40
|
+
details.values_at("base_branch", "head_branch").any? do |branch_name|
|
|
41
|
+
branch_name.end_with?("/#{branch}", "remotes/origin/#{branch}") ||
|
|
42
|
+
branch_name == branch
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# If multiple matching worktrees, prefer most recently created
|
|
47
|
+
# Use min_by to efficiently find the most recently created worktree
|
|
48
|
+
matching_worktree = matching_worktrees.min_by { |details| [-details["created_at"].to_i, details["path"]] }
|
|
49
|
+
|
|
50
|
+
validate_worktree_path(matching_worktree)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Helper method to validate worktree path and provide consistent logging
|
|
54
|
+
def validate_worktree_path(worktree_details)
|
|
55
|
+
return nil unless worktree_details
|
|
56
|
+
|
|
57
|
+
# Validate the worktree's integrity
|
|
58
|
+
if File.exist?(worktree_details["path"])
|
|
59
|
+
# Check if the worktree has the correct git repository
|
|
60
|
+
if valid_worktree_repository?(worktree_details["path"])
|
|
61
|
+
Aidp.log_debug("pr_worktree_manager", "found_existing_worktree",
|
|
62
|
+
path: worktree_details["path"],
|
|
63
|
+
base_branch: worktree_details["base_branch"],
|
|
64
|
+
head_branch: worktree_details["head_branch"])
|
|
65
|
+
return worktree_details["path"]
|
|
66
|
+
else
|
|
67
|
+
Aidp.log_warn("pr_worktree_manager", "corrupted_worktree",
|
|
68
|
+
pr_number: worktree_details["pr_number"] || "unknown",
|
|
69
|
+
path: worktree_details["path"])
|
|
70
|
+
end
|
|
71
|
+
else
|
|
72
|
+
Aidp.log_warn("pr_worktree_manager", "worktree_path_missing",
|
|
73
|
+
pr_number: worktree_details["pr_number"] || "unknown",
|
|
74
|
+
expected_path: worktree_details["path"])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Verify the integrity of the git worktree repository
|
|
81
|
+
def valid_worktree_repository?(worktree_path)
|
|
82
|
+
return false unless File.directory?(worktree_path)
|
|
83
|
+
|
|
84
|
+
# Check for .git directory or .git file (for submodules)
|
|
85
|
+
git_dir = File.join(worktree_path, ".git")
|
|
86
|
+
return false unless File.exist?(git_dir) || File.file?(git_dir)
|
|
87
|
+
|
|
88
|
+
true
|
|
89
|
+
rescue
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Create a new worktree for a PR
|
|
94
|
+
def create_worktree(pr_number, base_branch, head_branch, allow_duplicate: true, max_diff_size: nil)
|
|
95
|
+
# Log only the required attributes without max_diff_size
|
|
96
|
+
Aidp.log_debug(
|
|
97
|
+
"pr_worktree_manager", "creating_worktree",
|
|
98
|
+
pr_number: pr_number,
|
|
99
|
+
base_branch: base_branch,
|
|
100
|
+
head_branch: head_branch
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Validate inputs
|
|
104
|
+
raise ArgumentError, "PR number must be a positive integer" unless pr_number.to_i > 0
|
|
105
|
+
raise ArgumentError, "Base branch cannot be empty" if base_branch.nil? || base_branch.empty?
|
|
106
|
+
raise ArgumentError, "Head branch cannot be empty" if head_branch.nil? || head_branch.empty?
|
|
107
|
+
|
|
108
|
+
# Advanced max diff size handling
|
|
109
|
+
if max_diff_size
|
|
110
|
+
Aidp.log_debug(
|
|
111
|
+
"pr_worktree_manager", "diff_size_check",
|
|
112
|
+
method: "worktree_based_workflow"
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Check for existing worktrees if duplicates are not allowed
|
|
117
|
+
if !allow_duplicate
|
|
118
|
+
existing_worktrees = @worktrees.values.select do |details|
|
|
119
|
+
details["base_branch"] == base_branch && details["head_branch"] == head_branch
|
|
120
|
+
end
|
|
121
|
+
return existing_worktrees.first["path"] unless existing_worktrees.empty?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if a worktree for this PR already exists
|
|
125
|
+
existing_path = find_worktree(pr_number)
|
|
126
|
+
return existing_path if existing_path
|
|
127
|
+
|
|
128
|
+
# Generate a unique slug for the worktree
|
|
129
|
+
slug = "pr-#{pr_number}-#{Time.now.to_i}"
|
|
130
|
+
|
|
131
|
+
# Determine the base directory for worktrees
|
|
132
|
+
base_worktree_dir = @project_dir || File.expand_path("~/.aidp/worktrees")
|
|
133
|
+
worktree_path = File.join(base_worktree_dir, slug)
|
|
134
|
+
|
|
135
|
+
# Ensure base repo path is an actual git repository
|
|
136
|
+
raise "Not a git repository: #{@base_repo_path}" unless File.exist?(File.join(@base_repo_path, ".git"))
|
|
137
|
+
|
|
138
|
+
# Create the worktree directory if it doesn't exist
|
|
139
|
+
FileUtils.mkdir_p(base_worktree_dir)
|
|
140
|
+
|
|
141
|
+
# Verify base branch exists
|
|
142
|
+
Dir.chdir(@base_repo_path) do
|
|
143
|
+
# List all remote and local branches
|
|
144
|
+
branch_list_output = `git branch -a`.split("\n").map(&:strip)
|
|
145
|
+
|
|
146
|
+
# More robust branch existence check with expanded match criteria
|
|
147
|
+
base_branch_exists = branch_list_output.any? do |branch|
|
|
148
|
+
branch.end_with?("/#{base_branch}", "remotes/origin/#{base_branch}") ||
|
|
149
|
+
branch == base_branch ||
|
|
150
|
+
branch == "* #{base_branch}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Enhance branch tracking and fetching
|
|
154
|
+
unless base_branch_exists
|
|
155
|
+
# Try multiple fetch strategies
|
|
156
|
+
fetch_commands = [
|
|
157
|
+
"git fetch origin #{base_branch}:#{base_branch} 2>/dev/null",
|
|
158
|
+
"git fetch origin 2>/dev/null",
|
|
159
|
+
"git fetch --all 2>/dev/null"
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
fetch_commands.each do |fetch_cmd|
|
|
163
|
+
system(fetch_cmd)
|
|
164
|
+
branch_list_output = `git branch -a`.split("\n").map(&:strip)
|
|
165
|
+
base_branch_exists = branch_list_output.any? do |branch|
|
|
166
|
+
branch.end_with?("/#{base_branch}", "remotes/origin/#{base_branch}") ||
|
|
167
|
+
branch == base_branch ||
|
|
168
|
+
branch == "* #{base_branch}"
|
|
169
|
+
end
|
|
170
|
+
break if base_branch_exists
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
raise ArgumentError, "Base branch '#{base_branch}' does not exist in the repository" unless base_branch_exists
|
|
175
|
+
|
|
176
|
+
# Robust worktree creation with enhanced error handling and logging
|
|
177
|
+
worktree_create_command = "git worktree add #{Shellwords.escape(worktree_path)} -b #{Shellwords.escape(head_branch)} #{Shellwords.escape(base_branch)}"
|
|
178
|
+
unless system(worktree_create_command)
|
|
179
|
+
error_details = {
|
|
180
|
+
pr_number: pr_number,
|
|
181
|
+
base_branch: base_branch,
|
|
182
|
+
head_branch: head_branch,
|
|
183
|
+
command: worktree_create_command
|
|
184
|
+
}
|
|
185
|
+
Aidp.log_error(
|
|
186
|
+
"pr_worktree_manager", "worktree_creation_failed",
|
|
187
|
+
error_details
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Attempt to diagnose the failure
|
|
191
|
+
git_status = `git status`
|
|
192
|
+
Aidp.log_debug(
|
|
193
|
+
"pr_worktree_manager", "git_status_on_failure",
|
|
194
|
+
status: git_status
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
raise "Failed to create worktree for PR #{pr_number}"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Extended validation of worktree creation
|
|
202
|
+
unless File.exist?(worktree_path) && File.directory?(worktree_path)
|
|
203
|
+
error_details = {
|
|
204
|
+
pr_number: pr_number,
|
|
205
|
+
base_branch: base_branch,
|
|
206
|
+
head_branch: head_branch,
|
|
207
|
+
expected_path: worktree_path
|
|
208
|
+
}
|
|
209
|
+
Aidp.log_error(
|
|
210
|
+
"pr_worktree_manager", "worktree_path_validation_failed",
|
|
211
|
+
error_details
|
|
212
|
+
)
|
|
213
|
+
raise "Failed to validate worktree path for PR #{pr_number}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Prepare registry entry with additional metadata
|
|
217
|
+
registry_entry = {
|
|
218
|
+
"path" => worktree_path,
|
|
219
|
+
"base_branch" => base_branch,
|
|
220
|
+
"head_branch" => head_branch,
|
|
221
|
+
"created_at" => Time.now.to_i,
|
|
222
|
+
"slug" => slug,
|
|
223
|
+
"source" => "label_workflow" # Add custom source tracking
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# Conditionally add max_diff_size only if it's provided
|
|
227
|
+
registry_entry["max_diff_size"] = max_diff_size if max_diff_size
|
|
228
|
+
|
|
229
|
+
# Store in registry
|
|
230
|
+
@worktrees[pr_number.to_s] = registry_entry
|
|
231
|
+
save_registry
|
|
232
|
+
|
|
233
|
+
Aidp.log_debug(
|
|
234
|
+
"pr_worktree_manager", "worktree_created",
|
|
235
|
+
path: worktree_path,
|
|
236
|
+
pr_number: pr_number
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
worktree_path
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Enhanced method to extract changes from PR comments/reviews
|
|
243
|
+
def extract_pr_changes(changes_description)
|
|
244
|
+
Aidp.log_debug(
|
|
245
|
+
"pr_worktree_manager", "extracting_pr_changes",
|
|
246
|
+
description_length: changes_description&.length
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return nil if changes_description.nil? || changes_description.empty?
|
|
250
|
+
|
|
251
|
+
# Sophisticated change extraction with multiple parsing strategies
|
|
252
|
+
parsed_changes = {
|
|
253
|
+
files: [],
|
|
254
|
+
operations: [],
|
|
255
|
+
comments: [],
|
|
256
|
+
metadata: {}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# Advanced change detection patterns
|
|
260
|
+
file_patterns = [
|
|
261
|
+
/(?:modify|update|add|delete)\s+file:\s*([^\n]+)/i,
|
|
262
|
+
/\[(\w+)\]\s*([^\n]+)/, # GitHub-style change indicators
|
|
263
|
+
/(?:Action:\s*(\w+))\s*File:\s*([^\n]+)/i
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
# Operation mapping
|
|
267
|
+
operation_map = {
|
|
268
|
+
"add" => :create,
|
|
269
|
+
"create" => :create,
|
|
270
|
+
"update" => :modify,
|
|
271
|
+
"modify" => :modify,
|
|
272
|
+
"delete" => :delete,
|
|
273
|
+
"remove" => :delete
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Parse changes using multiple strategies
|
|
277
|
+
file_patterns.each do |pattern|
|
|
278
|
+
changes_description.scan(pattern) do |match|
|
|
279
|
+
operation = (match.size == 2) ? (match[0].downcase) : nil
|
|
280
|
+
file = (match.size == 2) ? (match[1].strip) : match[0].strip
|
|
281
|
+
|
|
282
|
+
parsed_changes[:files] << file
|
|
283
|
+
if operation && operation_map.key?(operation)
|
|
284
|
+
parsed_changes[:operations] << operation_map[operation]
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Extract potential comments or annotations
|
|
290
|
+
comment_pattern = /(?:comment|note):\s*(.+)/i
|
|
291
|
+
changes_description.scan(comment_pattern) do |match|
|
|
292
|
+
parsed_changes[:comments] << match[0].strip
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Additional metadata extraction
|
|
296
|
+
parsed_changes[:metadata] = {
|
|
297
|
+
source: "pr_comments",
|
|
298
|
+
timestamp: Time.now.to_i
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
Aidp.log_debug(
|
|
302
|
+
"pr_worktree_manager", "pr_changes_extracted",
|
|
303
|
+
files_count: parsed_changes[:files].size,
|
|
304
|
+
operations_count: parsed_changes[:operations].size,
|
|
305
|
+
comments_count: parsed_changes[:comments].size
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
parsed_changes
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Enhanced method to apply changes to the worktree with robust handling
|
|
312
|
+
def apply_worktree_changes(pr_number, changes)
|
|
313
|
+
Aidp.log_debug(
|
|
314
|
+
"pr_worktree_manager", "applying_worktree_changes",
|
|
315
|
+
pr_number: pr_number,
|
|
316
|
+
changes: changes
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Find the worktree for the PR
|
|
320
|
+
worktree_path = find_worktree(pr_number)
|
|
321
|
+
raise "No worktree found for PR #{pr_number}" unless worktree_path
|
|
322
|
+
|
|
323
|
+
# Track successful and failed file modifications
|
|
324
|
+
successful_files = []
|
|
325
|
+
failed_files = []
|
|
326
|
+
|
|
327
|
+
Dir.chdir(worktree_path) do
|
|
328
|
+
changes.fetch(:files, []).each_with_index do |file, index|
|
|
329
|
+
operation = changes.fetch(:operations, [])[index] || :modify
|
|
330
|
+
file_path = File.join(worktree_path, file)
|
|
331
|
+
|
|
332
|
+
# Enhanced file manipulation with operation-specific handling
|
|
333
|
+
begin
|
|
334
|
+
# Ensure safe file path (prevent directory traversal)
|
|
335
|
+
canonical_path = File.expand_path(file_path)
|
|
336
|
+
raise SecurityError, "Unsafe file path" unless canonical_path.start_with?(worktree_path)
|
|
337
|
+
|
|
338
|
+
# Ensure directory exists for file creation
|
|
339
|
+
FileUtils.mkdir_p(File.dirname(file_path)) unless File.exist?(File.dirname(file_path))
|
|
340
|
+
|
|
341
|
+
case operation
|
|
342
|
+
when :create, :modify
|
|
343
|
+
File.write(file_path, "# File #{(operation == :create) ? "added" : "modified"} by AIDP request-changes workflow\n")
|
|
344
|
+
when :delete
|
|
345
|
+
FileUtils.rm_f(file_path)
|
|
346
|
+
else
|
|
347
|
+
Aidp.log_warn(
|
|
348
|
+
"pr_worktree_manager", "unknown_file_operation",
|
|
349
|
+
file: file,
|
|
350
|
+
operation: operation
|
|
351
|
+
)
|
|
352
|
+
next
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
successful_files << file
|
|
356
|
+
Aidp.log_debug(
|
|
357
|
+
"pr_worktree_manager", "file_changed",
|
|
358
|
+
file: file,
|
|
359
|
+
action: operation
|
|
360
|
+
)
|
|
361
|
+
rescue SecurityError => e
|
|
362
|
+
Aidp.log_error(
|
|
363
|
+
"pr_worktree_manager", "file_path_security_error",
|
|
364
|
+
file: file,
|
|
365
|
+
error: e.message
|
|
366
|
+
)
|
|
367
|
+
failed_files << file
|
|
368
|
+
rescue => e
|
|
369
|
+
Aidp.log_error(
|
|
370
|
+
"pr_worktree_manager", "file_change_error",
|
|
371
|
+
file: file,
|
|
372
|
+
operation: operation,
|
|
373
|
+
error: e.message
|
|
374
|
+
)
|
|
375
|
+
failed_files << file
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Stage only successfully modified files
|
|
380
|
+
unless successful_files.empty?
|
|
381
|
+
system("git add #{successful_files.map { |f| Shellwords.escape(f) }.join(" ")}")
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
Aidp.log_debug(
|
|
386
|
+
"pr_worktree_manager", "worktree_changes_summary",
|
|
387
|
+
pr_number: pr_number,
|
|
388
|
+
successful_files_count: successful_files.size,
|
|
389
|
+
failed_files_count: failed_files.size,
|
|
390
|
+
total_files: changes.fetch(:files, []).size
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
{
|
|
394
|
+
success: successful_files.size == changes.fetch(:files, []).size,
|
|
395
|
+
successful_files: successful_files,
|
|
396
|
+
failed_files: failed_files
|
|
397
|
+
}
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Push changes back to the PR branch with enhanced error handling
|
|
401
|
+
def push_worktree_changes(pr_number, branch: nil)
|
|
402
|
+
Aidp.log_debug(
|
|
403
|
+
"pr_worktree_manager", "pushing_worktree_changes",
|
|
404
|
+
pr_number: pr_number,
|
|
405
|
+
branch: branch
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Find the worktree and its head branch
|
|
409
|
+
worktree_path = find_worktree(pr_number)
|
|
410
|
+
raise "No worktree found for PR #{pr_number}" unless worktree_path
|
|
411
|
+
|
|
412
|
+
# Retrieve the head branch from registry if not provided
|
|
413
|
+
head_branch = branch || @worktrees[pr_number.to_s]["head_branch"]
|
|
414
|
+
raise "No head branch found for PR #{pr_number}" unless head_branch
|
|
415
|
+
|
|
416
|
+
# Comprehensive error tracking
|
|
417
|
+
push_result = {
|
|
418
|
+
success: false,
|
|
419
|
+
git_actions: {
|
|
420
|
+
staged_changes: false,
|
|
421
|
+
committed: false,
|
|
422
|
+
pushed: false
|
|
423
|
+
},
|
|
424
|
+
errors: [],
|
|
425
|
+
changed_files: []
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
Dir.chdir(worktree_path) do
|
|
429
|
+
# Check staged changes with more robust capture
|
|
430
|
+
staged_changes_output = `git diff --staged --name-only`.strip
|
|
431
|
+
|
|
432
|
+
if !staged_changes_output.empty?
|
|
433
|
+
push_result[:git_actions][:staged_changes] = true
|
|
434
|
+
push_result[:changed_files] = staged_changes_output.split("\n")
|
|
435
|
+
|
|
436
|
+
# More robust commit command with additional logging
|
|
437
|
+
commit_message = "Changes applied via AIDP request-changes workflow for PR ##{pr_number}"
|
|
438
|
+
commit_command = "git commit -m '#{commit_message}' 2>&1"
|
|
439
|
+
commit_output = `#{commit_command}`.strip
|
|
440
|
+
|
|
441
|
+
if $?.success?
|
|
442
|
+
push_result[:git_actions][:committed] = true
|
|
443
|
+
|
|
444
|
+
# Enhanced push with verbose tracking
|
|
445
|
+
push_command = "git push origin #{head_branch} 2>&1"
|
|
446
|
+
push_output = `#{push_command}`.strip
|
|
447
|
+
|
|
448
|
+
if $?.success?
|
|
449
|
+
push_result[:git_actions][:pushed] = true
|
|
450
|
+
push_result[:success] = true
|
|
451
|
+
|
|
452
|
+
Aidp.log_debug(
|
|
453
|
+
"pr_worktree_manager", "changes_pushed_successfully",
|
|
454
|
+
pr_number: pr_number,
|
|
455
|
+
branch: head_branch,
|
|
456
|
+
changed_files_count: push_result[:changed_files].size
|
|
457
|
+
)
|
|
458
|
+
else
|
|
459
|
+
# Detailed push error logging
|
|
460
|
+
push_result[:errors] << "Push failed: #{push_output}"
|
|
461
|
+
Aidp.log_error(
|
|
462
|
+
"pr_worktree_manager", "push_changes_failed",
|
|
463
|
+
pr_number: pr_number,
|
|
464
|
+
branch: head_branch,
|
|
465
|
+
error_details: push_output
|
|
466
|
+
)
|
|
467
|
+
end
|
|
468
|
+
else
|
|
469
|
+
# Detailed commit error logging
|
|
470
|
+
push_result[:errors] << "Commit failed: #{commit_output}"
|
|
471
|
+
Aidp.log_error(
|
|
472
|
+
"pr_worktree_manager", "commit_changes_failed",
|
|
473
|
+
pr_number: pr_number,
|
|
474
|
+
branch: head_branch,
|
|
475
|
+
error_details: commit_output
|
|
476
|
+
)
|
|
477
|
+
end
|
|
478
|
+
else
|
|
479
|
+
# No changes to commit
|
|
480
|
+
push_result[:success] = true
|
|
481
|
+
Aidp.log_debug(
|
|
482
|
+
"pr_worktree_manager", "no_changes_to_push",
|
|
483
|
+
pr_number: pr_number
|
|
484
|
+
)
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
push_result
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Remove a specific worktree
|
|
492
|
+
def remove_worktree(pr_number)
|
|
493
|
+
Aidp.log_debug("pr_worktree_manager", "removing_worktree", pr_number: pr_number)
|
|
494
|
+
|
|
495
|
+
existing_worktree = @worktrees[pr_number.to_s]
|
|
496
|
+
return false unless existing_worktree
|
|
497
|
+
|
|
498
|
+
# Remove git worktree
|
|
499
|
+
system("git worktree remove #{existing_worktree["path"]}") if File.exist?(existing_worktree["path"])
|
|
500
|
+
|
|
501
|
+
# Remove from registry and save
|
|
502
|
+
@worktrees.delete(pr_number.to_s)
|
|
503
|
+
save_registry
|
|
504
|
+
|
|
505
|
+
true
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# List all active worktrees
|
|
509
|
+
def list_worktrees
|
|
510
|
+
# Include all known metadata keys from stored details
|
|
511
|
+
metadata_keys = ["path", "base_branch", "head_branch", "created_at", "max_diff_size"]
|
|
512
|
+
@worktrees.transform_values { |details| details.slice(*metadata_keys) }
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Cleanup old/stale worktrees (more than 30 days old)
|
|
516
|
+
def cleanup_stale_worktrees(days_threshold = 30)
|
|
517
|
+
Aidp.log_debug("pr_worktree_manager", "cleaning_stale_worktrees", threshold_days: days_threshold)
|
|
518
|
+
|
|
519
|
+
stale_worktrees = @worktrees.select do |_, details|
|
|
520
|
+
created_at = Time.at(details["created_at"])
|
|
521
|
+
(Time.now - created_at) > (days_threshold * 24 * 60 * 60)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
stale_worktrees.each_key { |pr_number| remove_worktree(pr_number) }
|
|
525
|
+
|
|
526
|
+
Aidp.log_debug(
|
|
527
|
+
"pr_worktree_manager", "stale_worktrees_cleaned",
|
|
528
|
+
count: stale_worktrees.size
|
|
529
|
+
)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
private
|
|
533
|
+
|
|
534
|
+
# Load the worktree registry from file
|
|
535
|
+
def load_registry
|
|
536
|
+
return {} unless File.exist?(worktree_registry_path)
|
|
537
|
+
|
|
538
|
+
begin
|
|
539
|
+
# Validate file content before parsing
|
|
540
|
+
registry_content = File.read(worktree_registry_path)
|
|
541
|
+
return {} if registry_content.strip.empty?
|
|
542
|
+
|
|
543
|
+
# Attempt to parse JSON
|
|
544
|
+
parsed_registry = JSON.parse(registry_content)
|
|
545
|
+
|
|
546
|
+
# Additional validation of registry structure
|
|
547
|
+
if parsed_registry.is_a?(Hash) && parsed_registry.all? { |k, v| k.is_a?(String) && v.is_a?(Hash) }
|
|
548
|
+
parsed_registry
|
|
549
|
+
else
|
|
550
|
+
Aidp.log_warn(
|
|
551
|
+
"pr_worktree_manager",
|
|
552
|
+
"invalid_registry_structure",
|
|
553
|
+
path: worktree_registry_path
|
|
554
|
+
)
|
|
555
|
+
{}
|
|
556
|
+
end
|
|
557
|
+
rescue JSON::ParserError
|
|
558
|
+
Aidp.log_warn(
|
|
559
|
+
"pr_worktree_manager",
|
|
560
|
+
"invalid_registry",
|
|
561
|
+
path: worktree_registry_path
|
|
562
|
+
)
|
|
563
|
+
{}
|
|
564
|
+
rescue SystemCallError
|
|
565
|
+
Aidp.log_warn(
|
|
566
|
+
"pr_worktree_manager",
|
|
567
|
+
"registry_read_error",
|
|
568
|
+
path: worktree_registry_path
|
|
569
|
+
)
|
|
570
|
+
{}
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# Save the worktree registry to file
|
|
575
|
+
def save_registry
|
|
576
|
+
FileUtils.mkdir_p(File.dirname(worktree_registry_path))
|
|
577
|
+
File.write(worktree_registry_path, JSON.pretty_generate(@worktrees))
|
|
578
|
+
rescue => e
|
|
579
|
+
Aidp.log_error("pr_worktree_manager", "registry_save_failed", error: e.message)
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
end
|
data/lib/aidp/util.rb
CHANGED
|
@@ -28,6 +28,17 @@ module Aidp
|
|
|
28
28
|
File.write(path, content)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Walk upward to find the nearest project root (git/package manager markers)
|
|
32
|
+
def self.find_project_root(start_dir = Dir.pwd)
|
|
33
|
+
dir = File.expand_path(start_dir)
|
|
34
|
+
until dir == File.dirname(dir)
|
|
35
|
+
return dir if project_root?(dir)
|
|
36
|
+
dir = File.dirname(dir)
|
|
37
|
+
end
|
|
38
|
+
# Fall back to the original directory when no markers were found
|
|
39
|
+
File.expand_path(start_dir)
|
|
40
|
+
end
|
|
41
|
+
|
|
31
42
|
def self.project_root?(dir = Dir.pwd)
|
|
32
43
|
File.exist?(File.join(dir, ".git")) ||
|
|
33
44
|
File.exist?(File.join(dir, "package.json")) ||
|
data/lib/aidp/version.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Aidp
|
|
|
12
12
|
@project_dir = project_dir
|
|
13
13
|
@logger = logger
|
|
14
14
|
@worktree_registry_path = File.join(project_dir, ".aidp", "worktrees.json")
|
|
15
|
+
@pr_worktree_registry_path = File.join(project_dir, ".aidp", "pr_worktrees.json")
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
# Find an existing worktree for a given branch or PR
|
|
@@ -20,7 +21,7 @@ module Aidp
|
|
|
20
21
|
|
|
21
22
|
raise WorktreeLookupError, "Invalid git repository: #{@project_dir}" unless git_repository?
|
|
22
23
|
|
|
23
|
-
#
|
|
24
|
+
# First, check registry first for exact branch match
|
|
24
25
|
worktree_info = read_registry.find { |w| w["branch"] == branch }
|
|
25
26
|
|
|
26
27
|
if worktree_info
|
|
@@ -42,6 +43,38 @@ module Aidp
|
|
|
42
43
|
raise
|
|
43
44
|
end
|
|
44
45
|
|
|
46
|
+
# Find or create a worktree for a PR, with advanced lookup strategies
|
|
47
|
+
def find_or_create_pr_worktree(pr_number:, head_branch:, base_branch: "main")
|
|
48
|
+
Aidp.log_debug("worktree_branch_manager", "finding_or_creating_pr_worktree",
|
|
49
|
+
pr_number: pr_number, head_branch: head_branch, base_branch: base_branch)
|
|
50
|
+
|
|
51
|
+
# First, check the PR-specific registry
|
|
52
|
+
pr_registry = read_pr_registry
|
|
53
|
+
pr_worktree = pr_registry.find { |w| w["pr_number"] == pr_number }
|
|
54
|
+
|
|
55
|
+
# If a valid worktree exists in the registry, return it
|
|
56
|
+
if pr_worktree
|
|
57
|
+
worktree_path = pr_worktree["path"]
|
|
58
|
+
return worktree_path if File.directory?(worktree_path)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Attempt to find worktree by branch name
|
|
62
|
+
existing_worktree = find_worktree(branch: head_branch)
|
|
63
|
+
return existing_worktree if existing_worktree
|
|
64
|
+
|
|
65
|
+
# If no existing worktree, create a new one
|
|
66
|
+
worktree_path = create_worktree(branch: head_branch, base_branch: base_branch)
|
|
67
|
+
|
|
68
|
+
# Update PR-specific registry
|
|
69
|
+
update_pr_registry(pr_number, head_branch, worktree_path, base_branch)
|
|
70
|
+
|
|
71
|
+
worktree_path
|
|
72
|
+
rescue => e
|
|
73
|
+
Aidp.log_error("worktree_branch_manager", "pr_worktree_creation_failed",
|
|
74
|
+
error: e.message, pr_number: pr_number, head_branch: head_branch)
|
|
75
|
+
raise
|
|
76
|
+
end
|
|
77
|
+
|
|
45
78
|
# Create a new worktree for a branch
|
|
46
79
|
def create_worktree(branch:, base_branch: "main")
|
|
47
80
|
Aidp.log_debug("worktree_branch_manager", "creating_worktree",
|
|
@@ -123,6 +156,19 @@ module Aidp
|
|
|
123
156
|
end
|
|
124
157
|
end
|
|
125
158
|
|
|
159
|
+
# Read the PR-specific worktree registry
|
|
160
|
+
def read_pr_registry
|
|
161
|
+
return [] unless File.exist?(@pr_worktree_registry_path)
|
|
162
|
+
|
|
163
|
+
begin
|
|
164
|
+
JSON.parse(File.read(@pr_worktree_registry_path))
|
|
165
|
+
rescue JSON::ParserError
|
|
166
|
+
Aidp.log_warn("worktree_branch_manager", "invalid_pr_registry",
|
|
167
|
+
path: @pr_worktree_registry_path)
|
|
168
|
+
[]
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
126
172
|
# Update the worktree registry
|
|
127
173
|
def update_registry(branch, path)
|
|
128
174
|
# Ensure .aidp directory exists
|
|
@@ -143,5 +189,28 @@ module Aidp
|
|
|
143
189
|
# Write updated registry
|
|
144
190
|
File.write(@worktree_registry_path, JSON.pretty_generate(registry))
|
|
145
191
|
end
|
|
192
|
+
|
|
193
|
+
# Update the PR-specific worktree registry
|
|
194
|
+
def update_pr_registry(pr_number, head_branch, worktree_path, base_branch)
|
|
195
|
+
# Ensure .aidp directory exists
|
|
196
|
+
FileUtils.mkdir_p(File.dirname(@pr_worktree_registry_path))
|
|
197
|
+
|
|
198
|
+
registry = read_pr_registry
|
|
199
|
+
|
|
200
|
+
# Remove existing entries for the same PR
|
|
201
|
+
registry.reject! { |w| w["pr_number"] == pr_number }
|
|
202
|
+
|
|
203
|
+
# Add new entry
|
|
204
|
+
registry << {
|
|
205
|
+
"pr_number" => pr_number,
|
|
206
|
+
"head_branch" => head_branch,
|
|
207
|
+
"base_branch" => base_branch,
|
|
208
|
+
"path" => worktree_path,
|
|
209
|
+
"created_at" => Time.now.to_i
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# Write updated registry
|
|
213
|
+
File.write(@pr_worktree_registry_path, JSON.pretty_generate(registry))
|
|
214
|
+
end
|
|
146
215
|
end
|
|
147
216
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aidp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.32.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -401,6 +401,7 @@ files:
|
|
|
401
401
|
- lib/aidp/planning/mappers/persona_mapper.rb
|
|
402
402
|
- lib/aidp/planning/parsers/document_parser.rb
|
|
403
403
|
- lib/aidp/planning/parsers/feedback_data_parser.rb
|
|
404
|
+
- lib/aidp/pr_worktree_manager.rb
|
|
404
405
|
- lib/aidp/prompt_optimization/context_composer.rb
|
|
405
406
|
- lib/aidp/prompt_optimization/optimizer.rb
|
|
406
407
|
- lib/aidp/prompt_optimization/prompt_builder.rb
|