aidp 0.30.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.
@@ -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,216 @@
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
+ @pr_worktree_registry_path = File.join(project_dir, ".aidp", "pr_worktrees.json")
16
+ end
17
+
18
+ # Find an existing worktree for a given branch or PR
19
+ def find_worktree(branch:)
20
+ Aidp.log_debug("worktree_branch_manager", "finding_worktree", branch: branch)
21
+
22
+ raise WorktreeLookupError, "Invalid git repository: #{@project_dir}" unless git_repository?
23
+
24
+ # First, check registry first for exact branch match
25
+ worktree_info = read_registry.find { |w| w["branch"] == branch }
26
+
27
+ if worktree_info
28
+ worktree_path = worktree_info["path"]
29
+ return worktree_path if File.directory?(worktree_path)
30
+ end
31
+
32
+ # Fallback: Use git worktree list to find the worktree
33
+ worktree_list_output = run_git_command("git worktree list")
34
+ worktree_list_output.split("\n").each do |line|
35
+ path, branch_info = line.split(" ", 2)
36
+ return path if branch_info&.include?(branch)
37
+ end
38
+
39
+ nil
40
+ rescue => e
41
+ Aidp.log_error("worktree_branch_manager", "worktree_lookup_failed",
42
+ error: e.message, branch: branch)
43
+ raise
44
+ end
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
+
78
+ # Create a new worktree for a branch
79
+ def create_worktree(branch:, base_branch: "main")
80
+ Aidp.log_debug("worktree_branch_manager", "creating_worktree",
81
+ branch: branch, base_branch: base_branch)
82
+
83
+ # Validate branch name to prevent path traversal
84
+ validate_branch_name!(branch)
85
+
86
+ # Check if worktree already exists
87
+ existing_worktree = find_worktree(branch: branch)
88
+ return existing_worktree if existing_worktree
89
+
90
+ # Ensure base branch exists
91
+ base_ref = (branch == "main") ? "main" : "refs/heads/#{base_branch}"
92
+ base_exists_cmd = "git show-ref --verify --quiet #{base_ref}"
93
+
94
+ system({"GIT_DIR" => File.join(@project_dir, ".git")}, "cd #{@project_dir} && #{base_exists_cmd}")
95
+
96
+ # If base branch doesn't exist locally, create it
97
+ unless $?.success?
98
+ system({"GIT_DIR" => File.join(@project_dir, ".git")}, "cd #{@project_dir} && git checkout -b #{base_branch}")
99
+ end
100
+
101
+ # Create worktree directory
102
+ worktree_name = branch.tr("/", "_")
103
+ worktree_path = File.join(@project_dir, ".worktrees", worktree_name)
104
+
105
+ # Ensure .worktrees directory exists
106
+ FileUtils.mkdir_p(File.join(@project_dir, ".worktrees"))
107
+
108
+ # Create the worktree
109
+ cmd = "git worktree add -b #{branch} #{worktree_path} #{base_branch}"
110
+ result = system({"GIT_DIR" => File.join(@project_dir, ".git")}, "cd #{@project_dir} && #{cmd}")
111
+
112
+ unless result
113
+ Aidp.log_error("worktree_branch_manager", "worktree_creation_failed",
114
+ branch: branch, base_branch: base_branch)
115
+ raise WorktreeCreationError, "Failed to create worktree for branch #{branch}"
116
+ end
117
+
118
+ # Update registry
119
+ update_registry(branch, worktree_path)
120
+
121
+ worktree_path
122
+ end
123
+
124
+ private
125
+
126
+ def git_repository?
127
+ File.directory?(File.join(@project_dir, ".git"))
128
+ rescue
129
+ false
130
+ end
131
+
132
+ def validate_branch_name!(branch)
133
+ if branch.include?("..") || branch.start_with?("/")
134
+ raise WorktreeCreationError, "Invalid branch name: #{branch}"
135
+ end
136
+ end
137
+
138
+ def run_git_command(cmd)
139
+ Dir.chdir(@project_dir) do
140
+ output = `#{cmd} 2>&1`
141
+ raise StandardError, output unless $?.success?
142
+ output
143
+ end
144
+ end
145
+
146
+ # Read the worktree registry
147
+ def read_registry
148
+ return [] unless File.exist?(@worktree_registry_path)
149
+
150
+ begin
151
+ JSON.parse(File.read(@worktree_registry_path))
152
+ rescue JSON::ParserError
153
+ Aidp.log_warn("worktree_branch_manager", "invalid_registry",
154
+ path: @worktree_registry_path)
155
+ []
156
+ end
157
+ end
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
+
172
+ # Update the worktree registry
173
+ def update_registry(branch, path)
174
+ # Ensure .aidp directory exists
175
+ FileUtils.mkdir_p(File.dirname(@worktree_registry_path))
176
+
177
+ registry = read_registry
178
+
179
+ # Remove existing entries for the same branch
180
+ registry.reject! { |w| w["branch"] == branch }
181
+
182
+ # Add new entry
183
+ registry << {
184
+ "branch" => branch,
185
+ "path" => path,
186
+ "created_at" => Time.now.to_i
187
+ }
188
+
189
+ # Write updated registry
190
+ File.write(@worktree_registry_path, JSON.pretty_generate(registry))
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
215
+ end
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.30.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
@@ -449,6 +450,8 @@ files:
449
450
  - lib/aidp/utils/devcontainer_detector.rb
450
451
  - lib/aidp/version.rb
451
452
  - lib/aidp/watch.rb
453
+ - lib/aidp/watch/auto_pr_processor.rb
454
+ - lib/aidp/watch/auto_processor.rb
452
455
  - lib/aidp/watch/build_processor.rb
453
456
  - lib/aidp/watch/change_request_processor.rb
454
457
  - lib/aidp/watch/ci_fix_processor.rb
@@ -473,6 +476,7 @@ files:
473
476
  - lib/aidp/workstream_executor.rb
474
477
  - lib/aidp/workstream_state.rb
475
478
  - lib/aidp/worktree.rb
479
+ - lib/aidp/worktree_branch_manager.rb
476
480
  - templates/COMMON/AGENT_BASE.md
477
481
  - templates/COMMON/CONVENTIONS.md
478
482
  - templates/COMMON/TEMPLATES/ADR_TEMPLATE.md