aidp 0.31.0 → 0.33.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/analyze/feature_analyzer.rb +322 -320
- data/lib/aidp/auto_update/coordinator.rb +97 -7
- data/lib/aidp/auto_update.rb +0 -12
- data/lib/aidp/cli/devcontainer_commands.rb +0 -5
- data/lib/aidp/cli.rb +2 -1
- data/lib/aidp/comment_consolidator.rb +78 -0
- data/lib/aidp/concurrency.rb +0 -3
- data/lib/aidp/config.rb +0 -1
- data/lib/aidp/config_paths.rb +71 -0
- data/lib/aidp/execute/work_loop_runner.rb +394 -15
- data/lib/aidp/harness/ai_filter_factory.rb +285 -0
- data/lib/aidp/harness/config_schema.rb +97 -1
- data/lib/aidp/harness/config_validator.rb +1 -1
- data/lib/aidp/harness/configuration.rb +61 -5
- data/lib/aidp/harness/filter_definition.rb +212 -0
- data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
- data/lib/aidp/harness/output_filter.rb +50 -25
- data/lib/aidp/harness/output_filter_config.rb +129 -0
- data/lib/aidp/harness/provider_manager.rb +128 -2
- data/lib/aidp/harness/provider_metrics.rb +5 -3
- data/lib/aidp/harness/runner.rb +0 -11
- data/lib/aidp/harness/test_runner.rb +179 -41
- data/lib/aidp/harness/thinking_depth_manager.rb +16 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +0 -2
- data/lib/aidp/loader.rb +195 -0
- data/lib/aidp/metadata/compiler.rb +29 -17
- data/lib/aidp/metadata/query.rb +1 -1
- data/lib/aidp/metadata/scanner.rb +8 -1
- data/lib/aidp/metadata/tool_metadata.rb +13 -13
- data/lib/aidp/metadata/validator.rb +10 -0
- data/lib/aidp/metadata.rb +16 -0
- data/lib/aidp/pr_worktree_manager.rb +582 -0
- data/lib/aidp/provider_manager.rb +1 -7
- data/lib/aidp/setup/wizard.rb +279 -9
- data/lib/aidp/skills.rb +0 -5
- data/lib/aidp/storage/csv_storage.rb +3 -0
- data/lib/aidp/style_guide/selector.rb +360 -0
- data/lib/aidp/tooling_detector.rb +283 -16
- data/lib/aidp/util.rb +11 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/change_request_processor.rb +152 -14
- data/lib/aidp/watch/repository_client.rb +41 -0
- data/lib/aidp/watch/runner.rb +29 -18
- data/lib/aidp/watch.rb +5 -7
- data/lib/aidp/workstream_cleanup.rb +0 -2
- data/lib/aidp/workstream_executor.rb +0 -4
- data/lib/aidp/worktree.rb +0 -1
- data/lib/aidp/worktree_branch_manager.rb +70 -1
- data/lib/aidp.rb +21 -106
- metadata +73 -36
- data/lib/aidp/config/paths.rb +0 -131
|
@@ -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
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "tty-prompt"
|
|
4
|
-
require_relative "harness/provider_factory"
|
|
5
4
|
|
|
6
5
|
module Aidp
|
|
7
6
|
class ProviderManager
|
|
@@ -20,12 +19,7 @@ module Aidp
|
|
|
20
19
|
|
|
21
20
|
# Get harness factory instance
|
|
22
21
|
def get_harness_factory
|
|
23
|
-
@harness_factory ||=
|
|
24
|
-
require_relative "harness/config_manager"
|
|
25
|
-
Aidp::Harness::ProviderFactory.new
|
|
26
|
-
rescue LoadError
|
|
27
|
-
nil
|
|
28
|
-
end
|
|
22
|
+
@harness_factory ||= Aidp::Harness::ProviderFactory.new
|
|
29
23
|
end
|
|
30
24
|
|
|
31
25
|
# Create provider using harness configuration
|