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.
@@ -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
- # Fetch diff to check size
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 diff_size > @config[:max_diff_size]
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
- # Check if a worktree already exists for this branch
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
- # Update @project_dir to point to the worktree
319
- @project_dir = existing[:path]
359
+ linked_issue_numbers.each do |issue_number|
360
+ workstream = @state_store.workstream_for_issue(issue_number)
361
+ next unless workstream
320
362
 
321
- # Pull latest changes in the worktree
322
- Dir.chdir(@project_dir) do
323
- run_git(%w[fetch origin], allow_failure: true)
324
- run_git(["checkout", head_ref])
325
- run_git(%w[pull --ff-only], allow_failure: true)
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
- return
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
- # Otherwise, use the main worktree
332
- Dir.chdir(@project_dir) do
333
- # Fetch latest
334
- run_git(%w[fetch origin])
385
+ nil
386
+ end
335
387
 
336
- # Checkout the PR branch
337
- run_git(["checkout", head_ref])
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
- # Pull latest changes
340
- run_git(%w[pull --ff-only], allow_failure: true)
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.log_debug("change_request_processor", "Checked out PR branch", branch: head_ref)
344
- display_message("🌿 Checked out branch: #{head_ref}", type: :info)
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 automated change requests.
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, please consider:
713
- 1. Breaking the PR into smaller chunks
714
- 2. Implementing changes manually
715
- 3. Increasing `max_diff_size` in your `aidp.yml` configuration if appropriate
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
@@ -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.29.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