aidp 0.29.0 → 0.31.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/config.rb +2 -8
- data/lib/aidp/harness/configuration.rb +1 -1
- data/lib/aidp/harness/deprecation_cache.rb +177 -0
- data/lib/aidp/harness/provider_manager.rb +36 -5
- data/lib/aidp/harness/ruby_llm_registry.rb +93 -5
- data/lib/aidp/harness/thinking_depth_manager.rb +47 -5
- data/lib/aidp/providers/anthropic.rb +186 -9
- data/lib/aidp/providers/cursor.rb +7 -1
- data/lib/aidp/setup/wizard.rb +65 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/auto_pr_processor.rb +86 -0
- data/lib/aidp/watch/auto_processor.rb +78 -0
- data/lib/aidp/watch/change_request_processor.rb +105 -27
- data/lib/aidp/watch/runner.rb +104 -0
- data/lib/aidp/watch/state_store.rb +37 -0
- data/lib/aidp/worktree_branch_manager.rb +147 -0
- metadata +5 -1
|
@@ -57,7 +57,8 @@ module Aidp
|
|
|
57
57
|
run_tests_before_push: true,
|
|
58
58
|
commit_message_prefix: "aidp: pr-change",
|
|
59
59
|
require_comment_reference: true,
|
|
60
|
-
max_diff_size: 2000
|
|
60
|
+
max_diff_size: 2000,
|
|
61
|
+
allow_large_pr_worktree_bypass: true # Default to always using worktree for large PRs
|
|
61
62
|
}.merge(symbolize_keys(change_request_config))
|
|
62
63
|
|
|
63
64
|
# Load safety configuration
|
|
@@ -95,16 +96,23 @@ module Aidp
|
|
|
95
96
|
return
|
|
96
97
|
end
|
|
97
98
|
|
|
98
|
-
#
|
|
99
|
+
# If max_diff_size is set, attempt to fetch and check diff
|
|
100
|
+
# But bypass restriction for worktree-based workflows
|
|
99
101
|
diff = @repository_client.fetch_pull_request_diff(number)
|
|
100
102
|
diff_size = diff.lines.count
|
|
101
103
|
|
|
102
|
-
if
|
|
104
|
+
# Check if we want to use the worktree bypass
|
|
105
|
+
use_worktree_bypass = @config[:allow_large_pr_worktree_bypass] || @config[:allow_large_pr_worktree_bypass].nil?
|
|
106
|
+
|
|
107
|
+
if diff_size > @config[:max_diff_size] && !use_worktree_bypass
|
|
103
108
|
display_message("⚠️ PR ##{number} diff too large (#{diff_size} lines > #{@config[:max_diff_size]}). Skipping.", type: :warn)
|
|
104
109
|
post_diff_too_large_comment(pr, diff_size)
|
|
105
110
|
return
|
|
106
111
|
end
|
|
107
112
|
|
|
113
|
+
# Log the diff size for observability
|
|
114
|
+
Aidp.log_debug("change_request_processor", "PR diff size", number: number, size: diff_size, max_allowed: @config[:max_diff_size], worktree_bypass: use_worktree_bypass)
|
|
115
|
+
|
|
108
116
|
# Analyze change requests
|
|
109
117
|
analysis_result = analyze_change_requests(pr_data: pr_data, comments: authorized_comments, diff: diff)
|
|
110
118
|
|
|
@@ -308,40 +316,105 @@ module Aidp
|
|
|
308
316
|
head_ref = pr_data[:head_ref]
|
|
309
317
|
pr_number = pr_data[:number]
|
|
310
318
|
|
|
311
|
-
|
|
319
|
+
worktree_path = resolve_worktree_for_pr(pr_data)
|
|
320
|
+
|
|
321
|
+
Dir.chdir(worktree_path) do
|
|
322
|
+
run_git(%w[fetch origin], allow_failure: true)
|
|
323
|
+
run_git(["checkout", head_ref])
|
|
324
|
+
run_git(%w[pull --ff-only], allow_failure: true)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
@project_dir = worktree_path
|
|
328
|
+
|
|
329
|
+
Aidp.log_debug("change_request_processor", "Checked out PR branch", branch: head_ref, worktree: worktree_path)
|
|
330
|
+
display_message("🌿 Using worktree for PR ##{pr_number}: #{head_ref}", type: :info)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def resolve_worktree_for_pr(pr_data)
|
|
334
|
+
head_ref = pr_data[:head_ref]
|
|
335
|
+
pr_number = pr_data[:number]
|
|
336
|
+
|
|
312
337
|
existing = Aidp::Worktree.find_by_branch(branch: head_ref, project_dir: @project_dir)
|
|
313
338
|
|
|
314
339
|
if existing && existing[:active]
|
|
315
340
|
display_message("🔄 Using existing worktree for branch: #{head_ref}", type: :info)
|
|
316
341
|
Aidp.log_debug("change_request_processor", "worktree_reused", pr_number: pr_number, branch: head_ref, path: existing[:path])
|
|
342
|
+
return existing[:path]
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
issue_worktree = find_issue_worktree_for_pr(pr_data)
|
|
346
|
+
return issue_worktree if issue_worktree
|
|
347
|
+
|
|
348
|
+
create_worktree_for_pr(pr_data)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def find_issue_worktree_for_pr(pr_data)
|
|
352
|
+
pr_number = pr_data[:number]
|
|
353
|
+
linked_issue_numbers = extract_issue_numbers_from_pr(pr_data)
|
|
354
|
+
|
|
355
|
+
build_match = @state_store.find_build_by_pr(pr_number)
|
|
356
|
+
linked_issue_numbers << build_match[:issue_number] if build_match
|
|
357
|
+
linked_issue_numbers = linked_issue_numbers.compact.uniq
|
|
317
358
|
|
|
318
|
-
|
|
319
|
-
|
|
359
|
+
linked_issue_numbers.each do |issue_number|
|
|
360
|
+
workstream = @state_store.workstream_for_issue(issue_number)
|
|
361
|
+
next unless workstream
|
|
320
362
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
363
|
+
slug = workstream[:workstream]
|
|
364
|
+
branch = workstream[:branch]
|
|
365
|
+
|
|
366
|
+
if slug
|
|
367
|
+
info = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
|
|
368
|
+
if info && info[:active]
|
|
369
|
+
Aidp.log_debug("change_request_processor", "issue_worktree_reused", pr_number: pr_number, issue_number: issue_number, branch: branch, path: info[:path])
|
|
370
|
+
display_message("🔄 Reusing worktree #{slug} for issue ##{issue_number} (PR ##{pr_number})", type: :info)
|
|
371
|
+
return info[:path]
|
|
372
|
+
end
|
|
326
373
|
end
|
|
327
374
|
|
|
328
|
-
|
|
375
|
+
if branch
|
|
376
|
+
existing = Aidp::Worktree.find_by_branch(branch: branch, project_dir: @project_dir)
|
|
377
|
+
if existing && existing[:active]
|
|
378
|
+
Aidp.log_debug("change_request_processor", "issue_branch_worktree_reused", pr_number: pr_number, issue_number: issue_number, branch: branch, path: existing[:path])
|
|
379
|
+
display_message("🔄 Reusing branch worktree for issue ##{issue_number}: #{branch}", type: :info)
|
|
380
|
+
return existing[:path]
|
|
381
|
+
end
|
|
382
|
+
end
|
|
329
383
|
end
|
|
330
384
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
# Fetch latest
|
|
334
|
-
run_git(%w[fetch origin])
|
|
385
|
+
nil
|
|
386
|
+
end
|
|
335
387
|
|
|
336
|
-
|
|
337
|
-
|
|
388
|
+
def extract_issue_numbers_from_pr(pr_data)
|
|
389
|
+
body = pr_data[:body].to_s
|
|
390
|
+
issue_matches = body.scan(/(?:Fixes|Resolves|Closes)\s+#(\d+)/i).flatten
|
|
338
391
|
|
|
339
|
-
|
|
340
|
-
|
|
392
|
+
issue_matches.map { |num| num.to_i }.uniq
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def create_worktree_for_pr(pr_data)
|
|
396
|
+
head_ref = pr_data[:head_ref]
|
|
397
|
+
pr_number = pr_data[:number]
|
|
398
|
+
slug = "pr-#{pr_number}-change-requests"
|
|
399
|
+
|
|
400
|
+
display_message("🌿 Creating worktree for PR ##{pr_number}: #{head_ref}", type: :info)
|
|
401
|
+
|
|
402
|
+
Dir.chdir(@project_dir) do
|
|
403
|
+
run_git(%w[fetch origin], allow_failure: true)
|
|
341
404
|
end
|
|
342
405
|
|
|
343
|
-
Aidp.
|
|
344
|
-
|
|
406
|
+
result = Aidp::Worktree.create(
|
|
407
|
+
slug: slug,
|
|
408
|
+
project_dir: @project_dir,
|
|
409
|
+
branch: head_ref,
|
|
410
|
+
base_branch: pr_data[:base_ref]
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
worktree_path = result[:path]
|
|
414
|
+
Aidp.log_debug("change_request_processor", "worktree_created", pr_number: pr_number, branch: head_ref, path: worktree_path)
|
|
415
|
+
display_message("✅ Worktree created at #{worktree_path}", type: :success)
|
|
416
|
+
|
|
417
|
+
worktree_path
|
|
345
418
|
end
|
|
346
419
|
|
|
347
420
|
def apply_changes(changes)
|
|
@@ -704,15 +777,20 @@ module Aidp
|
|
|
704
777
|
comment = <<~COMMENT
|
|
705
778
|
#{COMMENT_HEADER}
|
|
706
779
|
|
|
707
|
-
⚠️ PR diff is too large for
|
|
780
|
+
⚠️ PR diff is too large for default change requests.
|
|
708
781
|
|
|
709
782
|
**Current size:** #{diff_size} lines
|
|
710
783
|
**Maximum allowed:** #{@config[:max_diff_size]} lines
|
|
711
784
|
|
|
712
|
-
For large PRs,
|
|
713
|
-
1.
|
|
714
|
-
|
|
715
|
-
|
|
785
|
+
For large PRs, you have several options:
|
|
786
|
+
1. Enable worktree-based large PR handling:
|
|
787
|
+
Set `allow_large_pr_worktree_bypass: true` in your `aidp.yml`
|
|
788
|
+
2. Break the PR into smaller chunks
|
|
789
|
+
3. Implement changes manually
|
|
790
|
+
4. Increase `max_diff_size` in your configuration
|
|
791
|
+
|
|
792
|
+
The worktree bypass allows processing large PRs by working directly in the branch
|
|
793
|
+
instead of using diff-based changes.
|
|
716
794
|
COMMENT
|
|
717
795
|
|
|
718
796
|
begin
|
data/lib/aidp/watch/runner.rb
CHANGED
|
@@ -14,6 +14,8 @@ require_relative "../auto_update"
|
|
|
14
14
|
require_relative "review_processor"
|
|
15
15
|
require_relative "ci_fix_processor"
|
|
16
16
|
require_relative "change_request_processor"
|
|
17
|
+
require_relative "auto_processor"
|
|
18
|
+
require_relative "auto_pr_processor"
|
|
17
19
|
|
|
18
20
|
module Aidp
|
|
19
21
|
module Watch
|
|
@@ -70,6 +72,13 @@ module Aidp
|
|
|
70
72
|
verbose: verbose,
|
|
71
73
|
label_config: label_config
|
|
72
74
|
)
|
|
75
|
+
@auto_processor = AutoProcessor.new(
|
|
76
|
+
repository_client: @repository_client,
|
|
77
|
+
state_store: @state_store,
|
|
78
|
+
build_processor: @build_processor,
|
|
79
|
+
label_config: label_config,
|
|
80
|
+
verbose: verbose
|
|
81
|
+
)
|
|
73
82
|
|
|
74
83
|
# Initialize auto-update coordinator
|
|
75
84
|
@auto_update_coordinator = Aidp::AutoUpdate.coordinator(project_dir: project_dir)
|
|
@@ -90,6 +99,14 @@ module Aidp
|
|
|
90
99
|
label_config: label_config,
|
|
91
100
|
verbose: verbose
|
|
92
101
|
)
|
|
102
|
+
@auto_pr_processor = AutoPrProcessor.new(
|
|
103
|
+
repository_client: @repository_client,
|
|
104
|
+
state_store: @state_store,
|
|
105
|
+
review_processor: @review_processor,
|
|
106
|
+
ci_fix_processor: @ci_fix_processor,
|
|
107
|
+
label_config: label_config,
|
|
108
|
+
verbose: verbose
|
|
109
|
+
)
|
|
93
110
|
@change_request_processor = ChangeRequestProcessor.new(
|
|
94
111
|
repository_client: @repository_client,
|
|
95
112
|
state_store: @state_store,
|
|
@@ -142,9 +159,11 @@ module Aidp
|
|
|
142
159
|
def process_cycle
|
|
143
160
|
process_plan_triggers
|
|
144
161
|
process_build_triggers
|
|
162
|
+
process_auto_issue_triggers
|
|
145
163
|
check_for_updates_if_due
|
|
146
164
|
process_review_triggers
|
|
147
165
|
process_ci_fix_triggers
|
|
166
|
+
process_auto_pr_triggers
|
|
148
167
|
process_change_request_triggers
|
|
149
168
|
end
|
|
150
169
|
|
|
@@ -264,6 +283,54 @@ module Aidp
|
|
|
264
283
|
end
|
|
265
284
|
end
|
|
266
285
|
|
|
286
|
+
def process_auto_issue_triggers
|
|
287
|
+
auto_label = @auto_processor.auto_label
|
|
288
|
+
begin
|
|
289
|
+
issues = @repository_client.list_issues(labels: [auto_label], state: "open")
|
|
290
|
+
rescue => e
|
|
291
|
+
Aidp.log_error("watch_runner", "auto_issue_poll_failed", label: auto_label, error: e.message)
|
|
292
|
+
return
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
Aidp.log_debug("watch_runner", "auto_issue_poll", label: auto_label, total: issues.size)
|
|
296
|
+
|
|
297
|
+
issues.each do |issue|
|
|
298
|
+
unless issue_has_label?(issue, auto_label)
|
|
299
|
+
Aidp.log_debug("watch_runner", "auto_issue_skip_label_mismatch", issue: issue[:number], labels: issue[:labels])
|
|
300
|
+
next
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
begin
|
|
304
|
+
detailed = @repository_client.fetch_issue(issue[:number])
|
|
305
|
+
rescue => e
|
|
306
|
+
Aidp.log_error("watch_runner", "auto_issue_fetch_failed", issue: issue[:number], error: e.message)
|
|
307
|
+
next
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Check if already in progress by another instance
|
|
311
|
+
if @state_extractor.in_progress?(detailed)
|
|
312
|
+
Aidp.log_debug("watch_runner", "auto_issue_skip_in_progress", issue: detailed[:number])
|
|
313
|
+
next
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Check author authorization before processing
|
|
317
|
+
unless @safety_checker.should_process_issue?(detailed, enforce: false)
|
|
318
|
+
Aidp.log_debug("watch_runner", "auto_issue_skip_unauthorized_author", issue: detailed[:number], author: detailed[:author])
|
|
319
|
+
next
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Check if detection comment already posted (deduplication)
|
|
323
|
+
unless @state_extractor.detection_comment_posted?(detailed, auto_label)
|
|
324
|
+
post_detection_comment(item_type: :issue, number: detailed[:number], label: auto_label)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
Aidp.log_debug("watch_runner", "auto_issue_process", issue: detailed[:number])
|
|
328
|
+
@auto_processor.process(detailed)
|
|
329
|
+
rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
|
|
330
|
+
Aidp.log_warn("watch_runner", "unauthorized_issue_author_auto", issue: issue[:number], error: e.message)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
267
334
|
def process_review_triggers
|
|
268
335
|
review_label = @review_processor.review_label
|
|
269
336
|
begin
|
|
@@ -310,6 +377,43 @@ module Aidp
|
|
|
310
377
|
end
|
|
311
378
|
end
|
|
312
379
|
|
|
380
|
+
def process_auto_pr_triggers
|
|
381
|
+
auto_label = @auto_pr_processor.auto_label
|
|
382
|
+
prs = @repository_client.list_pull_requests(labels: [auto_label], state: "open")
|
|
383
|
+
Aidp.log_debug("watch_runner", "auto_pr_poll", label: auto_label, total: prs.size)
|
|
384
|
+
|
|
385
|
+
prs.each do |pr|
|
|
386
|
+
unless pr_has_label?(pr, auto_label)
|
|
387
|
+
Aidp.log_debug("watch_runner", "auto_pr_skip_label_mismatch", pr: pr[:number], labels: pr[:labels])
|
|
388
|
+
next
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
detailed = @repository_client.fetch_pull_request(pr[:number])
|
|
392
|
+
|
|
393
|
+
# Check if already in progress by another instance
|
|
394
|
+
if @state_extractor.in_progress?(detailed)
|
|
395
|
+
Aidp.log_debug("watch_runner", "auto_pr_skip_in_progress", pr: detailed[:number])
|
|
396
|
+
next
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Check author authorization before processing
|
|
400
|
+
unless @safety_checker.should_process_issue?(detailed, enforce: false)
|
|
401
|
+
Aidp.log_debug("watch_runner", "auto_pr_skip_unauthorized_author", pr: detailed[:number], author: detailed[:author])
|
|
402
|
+
next
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Check if detection comment already posted (deduplication)
|
|
406
|
+
unless @state_extractor.detection_comment_posted?(detailed, auto_label)
|
|
407
|
+
post_detection_comment(item_type: :pr, number: detailed[:number], label: auto_label)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
Aidp.log_debug("watch_runner", "auto_pr_process", pr: detailed[:number])
|
|
411
|
+
@auto_pr_processor.process(detailed)
|
|
412
|
+
rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
|
|
413
|
+
Aidp.log_warn("watch_runner", "unauthorized_pr_author_auto", pr: pr[:number], error: e.message)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
313
417
|
def process_ci_fix_triggers
|
|
314
418
|
ci_fix_label = @ci_fix_processor.ci_fix_label
|
|
315
419
|
prs = @repository_client.list_pull_requests(labels: [ci_fix_label], state: "open")
|
|
@@ -65,6 +65,43 @@ module Aidp
|
|
|
65
65
|
save!
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# Retrieve workstream metadata for a given issue
|
|
69
|
+
# @return [Hash, nil] {issue_number:, branch:, workstream:, pr_url:, status:}
|
|
70
|
+
def workstream_for_issue(issue_number)
|
|
71
|
+
data = build_status(issue_number)
|
|
72
|
+
return nil if data.nil? || data.empty?
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
issue_number: issue_number.to_i,
|
|
76
|
+
branch: data["branch"],
|
|
77
|
+
workstream: data["workstream"],
|
|
78
|
+
pr_url: data["pr_url"],
|
|
79
|
+
status: data["status"]
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Find the build/workstream metadata associated with a PR URL
|
|
84
|
+
# This is used to map change-request PRs back to their originating issues/worktrees.
|
|
85
|
+
# @return [Hash, nil] {issue_number:, branch:, workstream:, pr_url:, status:}
|
|
86
|
+
def find_build_by_pr(pr_number)
|
|
87
|
+
builds.each do |issue_number, data|
|
|
88
|
+
pr_url = data["pr_url"]
|
|
89
|
+
next unless pr_url
|
|
90
|
+
|
|
91
|
+
if pr_url.match?(%r{/pull/#{pr_number}\b})
|
|
92
|
+
return {
|
|
93
|
+
issue_number: issue_number.to_i,
|
|
94
|
+
branch: data["branch"],
|
|
95
|
+
workstream: data["workstream"],
|
|
96
|
+
pr_url: pr_url,
|
|
97
|
+
status: data["status"]
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
68
105
|
# Review tracking methods
|
|
69
106
|
def review_processed?(pr_number)
|
|
70
107
|
reviews.key?(pr_number.to_s)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
module Aidp
|
|
5
|
+
# Manages git worktrees for pull request branches
|
|
6
|
+
class WorktreeBranchManager
|
|
7
|
+
class WorktreeCreationError < StandardError; end
|
|
8
|
+
class WorktreeLookupError < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Initialize with a project directory and optional logger
|
|
11
|
+
def initialize(project_dir:, logger: Aidp.logger)
|
|
12
|
+
@project_dir = project_dir
|
|
13
|
+
@logger = logger
|
|
14
|
+
@worktree_registry_path = File.join(project_dir, ".aidp", "worktrees.json")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Find an existing worktree for a given branch or PR
|
|
18
|
+
def find_worktree(branch:)
|
|
19
|
+
Aidp.log_debug("worktree_branch_manager", "finding_worktree", branch: branch)
|
|
20
|
+
|
|
21
|
+
raise WorktreeLookupError, "Invalid git repository: #{@project_dir}" unless git_repository?
|
|
22
|
+
|
|
23
|
+
# Check registry first
|
|
24
|
+
worktree_info = read_registry.find { |w| w["branch"] == branch }
|
|
25
|
+
|
|
26
|
+
if worktree_info
|
|
27
|
+
worktree_path = worktree_info["path"]
|
|
28
|
+
return worktree_path if File.directory?(worktree_path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Fallback: Use git worktree list to find the worktree
|
|
32
|
+
worktree_list_output = run_git_command("git worktree list")
|
|
33
|
+
worktree_list_output.split("\n").each do |line|
|
|
34
|
+
path, branch_info = line.split(" ", 2)
|
|
35
|
+
return path if branch_info&.include?(branch)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
nil
|
|
39
|
+
rescue => e
|
|
40
|
+
Aidp.log_error("worktree_branch_manager", "worktree_lookup_failed",
|
|
41
|
+
error: e.message, branch: branch)
|
|
42
|
+
raise
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Create a new worktree for a branch
|
|
46
|
+
def create_worktree(branch:, base_branch: "main")
|
|
47
|
+
Aidp.log_debug("worktree_branch_manager", "creating_worktree",
|
|
48
|
+
branch: branch, base_branch: base_branch)
|
|
49
|
+
|
|
50
|
+
# Validate branch name to prevent path traversal
|
|
51
|
+
validate_branch_name!(branch)
|
|
52
|
+
|
|
53
|
+
# Check if worktree already exists
|
|
54
|
+
existing_worktree = find_worktree(branch: branch)
|
|
55
|
+
return existing_worktree if existing_worktree
|
|
56
|
+
|
|
57
|
+
# Ensure base branch exists
|
|
58
|
+
base_ref = (branch == "main") ? "main" : "refs/heads/#{base_branch}"
|
|
59
|
+
base_exists_cmd = "git show-ref --verify --quiet #{base_ref}"
|
|
60
|
+
|
|
61
|
+
system({"GIT_DIR" => File.join(@project_dir, ".git")}, "cd #{@project_dir} && #{base_exists_cmd}")
|
|
62
|
+
|
|
63
|
+
# If base branch doesn't exist locally, create it
|
|
64
|
+
unless $?.success?
|
|
65
|
+
system({"GIT_DIR" => File.join(@project_dir, ".git")}, "cd #{@project_dir} && git checkout -b #{base_branch}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Create worktree directory
|
|
69
|
+
worktree_name = branch.tr("/", "_")
|
|
70
|
+
worktree_path = File.join(@project_dir, ".worktrees", worktree_name)
|
|
71
|
+
|
|
72
|
+
# Ensure .worktrees directory exists
|
|
73
|
+
FileUtils.mkdir_p(File.join(@project_dir, ".worktrees"))
|
|
74
|
+
|
|
75
|
+
# Create the worktree
|
|
76
|
+
cmd = "git worktree add -b #{branch} #{worktree_path} #{base_branch}"
|
|
77
|
+
result = system({"GIT_DIR" => File.join(@project_dir, ".git")}, "cd #{@project_dir} && #{cmd}")
|
|
78
|
+
|
|
79
|
+
unless result
|
|
80
|
+
Aidp.log_error("worktree_branch_manager", "worktree_creation_failed",
|
|
81
|
+
branch: branch, base_branch: base_branch)
|
|
82
|
+
raise WorktreeCreationError, "Failed to create worktree for branch #{branch}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Update registry
|
|
86
|
+
update_registry(branch, worktree_path)
|
|
87
|
+
|
|
88
|
+
worktree_path
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def git_repository?
|
|
94
|
+
File.directory?(File.join(@project_dir, ".git"))
|
|
95
|
+
rescue
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def validate_branch_name!(branch)
|
|
100
|
+
if branch.include?("..") || branch.start_with?("/")
|
|
101
|
+
raise WorktreeCreationError, "Invalid branch name: #{branch}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def run_git_command(cmd)
|
|
106
|
+
Dir.chdir(@project_dir) do
|
|
107
|
+
output = `#{cmd} 2>&1`
|
|
108
|
+
raise StandardError, output unless $?.success?
|
|
109
|
+
output
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Read the worktree registry
|
|
114
|
+
def read_registry
|
|
115
|
+
return [] unless File.exist?(@worktree_registry_path)
|
|
116
|
+
|
|
117
|
+
begin
|
|
118
|
+
JSON.parse(File.read(@worktree_registry_path))
|
|
119
|
+
rescue JSON::ParserError
|
|
120
|
+
Aidp.log_warn("worktree_branch_manager", "invalid_registry",
|
|
121
|
+
path: @worktree_registry_path)
|
|
122
|
+
[]
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Update the worktree registry
|
|
127
|
+
def update_registry(branch, path)
|
|
128
|
+
# Ensure .aidp directory exists
|
|
129
|
+
FileUtils.mkdir_p(File.dirname(@worktree_registry_path))
|
|
130
|
+
|
|
131
|
+
registry = read_registry
|
|
132
|
+
|
|
133
|
+
# Remove existing entries for the same branch
|
|
134
|
+
registry.reject! { |w| w["branch"] == branch }
|
|
135
|
+
|
|
136
|
+
# Add new entry
|
|
137
|
+
registry << {
|
|
138
|
+
"branch" => branch,
|
|
139
|
+
"path" => path,
|
|
140
|
+
"created_at" => Time.now.to_i
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Write updated registry
|
|
144
|
+
File.write(@worktree_registry_path, JSON.pretty_generate(registry))
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
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.31.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -324,6 +324,7 @@ files:
|
|
|
324
324
|
- lib/aidp/harness/config_schema.rb
|
|
325
325
|
- lib/aidp/harness/config_validator.rb
|
|
326
326
|
- lib/aidp/harness/configuration.rb
|
|
327
|
+
- lib/aidp/harness/deprecation_cache.rb
|
|
327
328
|
- lib/aidp/harness/enhanced_runner.rb
|
|
328
329
|
- lib/aidp/harness/error_handler.rb
|
|
329
330
|
- lib/aidp/harness/filter_strategy.rb
|
|
@@ -448,6 +449,8 @@ files:
|
|
|
448
449
|
- lib/aidp/utils/devcontainer_detector.rb
|
|
449
450
|
- lib/aidp/version.rb
|
|
450
451
|
- lib/aidp/watch.rb
|
|
452
|
+
- lib/aidp/watch/auto_pr_processor.rb
|
|
453
|
+
- lib/aidp/watch/auto_processor.rb
|
|
451
454
|
- lib/aidp/watch/build_processor.rb
|
|
452
455
|
- lib/aidp/watch/change_request_processor.rb
|
|
453
456
|
- lib/aidp/watch/ci_fix_processor.rb
|
|
@@ -472,6 +475,7 @@ files:
|
|
|
472
475
|
- lib/aidp/workstream_executor.rb
|
|
473
476
|
- lib/aidp/workstream_state.rb
|
|
474
477
|
- lib/aidp/worktree.rb
|
|
478
|
+
- lib/aidp/worktree_branch_manager.rb
|
|
475
479
|
- templates/COMMON/AGENT_BASE.md
|
|
476
480
|
- templates/COMMON/CONVENTIONS.md
|
|
477
481
|
- templates/COMMON/TEMPLATES/ADR_TEMPLATE.md
|