aidp 0.33.0 → 0.34.1

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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
  4. data/lib/aidp/cli/eval_command.rb +399 -0
  5. data/lib/aidp/cli/harness_command.rb +1 -1
  6. data/lib/aidp/cli/security_command.rb +416 -0
  7. data/lib/aidp/cli/tools_command.rb +6 -4
  8. data/lib/aidp/cli.rb +170 -3
  9. data/lib/aidp/concurrency/exec.rb +3 -0
  10. data/lib/aidp/config.rb +113 -0
  11. data/lib/aidp/config_paths.rb +20 -0
  12. data/lib/aidp/daemon/runner.rb +8 -4
  13. data/lib/aidp/errors.rb +134 -0
  14. data/lib/aidp/evaluations/context_capture.rb +205 -0
  15. data/lib/aidp/evaluations/evaluation_record.rb +114 -0
  16. data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
  17. data/lib/aidp/evaluations.rb +23 -0
  18. data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
  19. data/lib/aidp/execute/interactive_repl.rb +6 -2
  20. data/lib/aidp/execute/prompt_evaluator.rb +359 -0
  21. data/lib/aidp/execute/repl_macros.rb +100 -1
  22. data/lib/aidp/execute/work_loop_runner.rb +399 -47
  23. data/lib/aidp/execute/work_loop_state.rb +4 -1
  24. data/lib/aidp/execute/workflow_selector.rb +3 -0
  25. data/lib/aidp/harness/ai_decision_engine.rb +79 -0
  26. data/lib/aidp/harness/capability_registry.rb +2 -0
  27. data/lib/aidp/harness/condition_detector.rb +3 -0
  28. data/lib/aidp/harness/config_loader.rb +3 -0
  29. data/lib/aidp/harness/enhanced_runner.rb +14 -11
  30. data/lib/aidp/harness/error_handler.rb +3 -0
  31. data/lib/aidp/harness/provider_factory.rb +3 -0
  32. data/lib/aidp/harness/provider_manager.rb +6 -0
  33. data/lib/aidp/harness/runner.rb +5 -1
  34. data/lib/aidp/harness/state/persistence.rb +3 -0
  35. data/lib/aidp/harness/state_manager.rb +3 -0
  36. data/lib/aidp/harness/status_display.rb +28 -20
  37. data/lib/aidp/harness/thinking_depth_manager.rb +32 -32
  38. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
  39. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
  40. data/lib/aidp/harness/ui/error_handler.rb +3 -0
  41. data/lib/aidp/harness/ui/job_monitor.rb +4 -0
  42. data/lib/aidp/harness/ui/navigation/submenu.rb +2 -0
  43. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
  44. data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
  45. data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
  46. data/lib/aidp/harness/ui.rb +11 -0
  47. data/lib/aidp/harness/user_interface.rb +3 -0
  48. data/lib/aidp/loader.rb +2 -2
  49. data/lib/aidp/logger.rb +3 -0
  50. data/lib/aidp/message_display.rb +31 -0
  51. data/lib/aidp/pr_worktree_manager.rb +18 -6
  52. data/lib/aidp/provider_manager.rb +3 -0
  53. data/lib/aidp/providers/base.rb +2 -0
  54. data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
  55. data/lib/aidp/security/secrets_proxy.rb +328 -0
  56. data/lib/aidp/security/secrets_registry.rb +227 -0
  57. data/lib/aidp/security/trifecta_state.rb +220 -0
  58. data/lib/aidp/security/watch_mode_handler.rb +306 -0
  59. data/lib/aidp/security/work_loop_adapter.rb +277 -0
  60. data/lib/aidp/security.rb +56 -0
  61. data/lib/aidp/setup/wizard.rb +4 -2
  62. data/lib/aidp/version.rb +1 -1
  63. data/lib/aidp/watch/auto_merger.rb +274 -0
  64. data/lib/aidp/watch/auto_pr_processor.rb +125 -7
  65. data/lib/aidp/watch/build_processor.rb +16 -1
  66. data/lib/aidp/watch/change_request_processor.rb +680 -286
  67. data/lib/aidp/watch/ci_fix_processor.rb +262 -4
  68. data/lib/aidp/watch/feedback_collector.rb +191 -0
  69. data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
  70. data/lib/aidp/watch/implementation_verifier.rb +142 -1
  71. data/lib/aidp/watch/plan_generator.rb +70 -13
  72. data/lib/aidp/watch/plan_processor.rb +12 -5
  73. data/lib/aidp/watch/projects_processor.rb +286 -0
  74. data/lib/aidp/watch/repository_client.rb +861 -53
  75. data/lib/aidp/watch/review_processor.rb +33 -6
  76. data/lib/aidp/watch/runner.rb +51 -11
  77. data/lib/aidp/watch/state_store.rb +233 -0
  78. data/lib/aidp/watch/sub_issue_creator.rb +221 -0
  79. data/lib/aidp/workflows/guided_agent.rb +4 -0
  80. data/lib/aidp/workstream_executor.rb +3 -0
  81. data/lib/aidp/worktree.rb +61 -11
  82. data/lib/aidp/worktree_branch_manager.rb +347 -101
  83. data/templates/implementation/iterative_implementation.md +46 -3
  84. metadata +21 -1
@@ -13,6 +13,8 @@ require_relative "../harness/runner"
13
13
  require_relative "../harness/state_manager"
14
14
  require_relative "../harness/test_runner"
15
15
  require_relative "../worktree"
16
+ require_relative "../pr_worktree_manager"
17
+ require_relative "../worktree_branch_manager"
16
18
  require_relative "github_state_extractor"
17
19
  require_relative "implementation_verifier"
18
20
 
@@ -32,6 +34,9 @@ module Aidp
32
34
 
33
35
  attr_reader :change_request_label, :needs_input_label
34
36
 
37
+ # Expose state for testability
38
+ attr_accessor :project_dir, :worktree_branch_manager
39
+
35
40
  def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, change_request_config: {}, safety_config: {}, verbose: false)
36
41
  @repository_client = repository_client
37
42
  @state_store = state_store
@@ -40,11 +45,9 @@ module Aidp
40
45
  @project_dir = project_dir
41
46
  @verbose = verbose
42
47
 
43
- # Log initialization details
44
- Aidp.log_debug("change_request_processor", "initializing",
45
- provider_name: provider_name,
46
- project_dir: project_dir,
47
- verbose: verbose)
48
+ # Initialize worktree managers
49
+ @pr_worktree_manager = Aidp::PRWorktreeManager.new(project_dir: project_dir)
50
+ @worktree_branch_manager = Aidp::WorktreeBranchManager.new(project_dir: project_dir)
48
51
 
49
52
  # Initialize verifier
50
53
  @verifier = ImplementationVerifier.new(
@@ -56,11 +59,6 @@ module Aidp
56
59
  @change_request_label = label_config[:change_request_trigger] || label_config["change_request_trigger"] || DEFAULT_CHANGE_REQUEST_LABEL
57
60
  @needs_input_label = label_config[:needs_input] || label_config["needs_input"] || DEFAULT_NEEDS_INPUT_LABEL
58
61
 
59
- # Log label details
60
- Aidp.log_debug("change_request_processor", "label_configuration",
61
- change_request_label: @change_request_label,
62
- needs_input_label: @needs_input_label)
63
-
64
62
  # Load change request configuration
65
63
  @config = {
66
64
  enabled: true,
@@ -69,40 +67,62 @@ module Aidp
69
67
  commit_message_prefix: "aidp: pr-change",
70
68
  require_comment_reference: true,
71
69
  max_diff_size: 2000,
72
- allow_large_pr_worktree_bypass: true # Default to always using worktree for large PRs
70
+ large_pr_strategy: "create_worktree", # Options: create_worktree, manual, skip
71
+ worktree_strategy: "auto", # Options: auto, always_create, reuse_only
72
+ worktree_max_age: 7 * 24 * 60 * 60, # 7 days in seconds
73
+ worktree_cleanup_on_success: true,
74
+ worktree_cleanup_on_failure: false
73
75
  }.merge(symbolize_keys(change_request_config))
74
76
 
75
- # Log configuration details
76
- Aidp.log_debug("change_request_processor", "change_request_config",
77
- config: @config.transform_values { |v| v.is_a?(Proc) ? "Proc" : v })
77
+ # Log the configuration, especially the large PR strategy
78
+ Aidp.log_debug(
79
+ "change_request_processor", "Initialized with config",
80
+ max_diff_size: @config[:max_diff_size],
81
+ large_pr_strategy: @config[:large_pr_strategy],
82
+ run_tests_before_push: @config[:run_tests_before_push],
83
+ enabled: @config[:enabled],
84
+ worktree_strategy: @config[:worktree_strategy],
85
+ worktree_max_age: @config[:worktree_max_age],
86
+ worktree_cleanup_on_success: @config[:worktree_cleanup_on_success],
87
+ worktree_cleanup_on_failure: @config[:worktree_cleanup_on_failure]
88
+ )
78
89
 
79
90
  # Load safety configuration
80
91
  @safety_config = safety_config
81
92
  @author_allowlist = Array(@safety_config[:author_allowlist] || @safety_config["author_allowlist"])
82
-
83
- # Log safety configuration
84
- Aidp.log_debug("change_request_processor", "safety_configuration",
85
- author_allowlist: @author_allowlist,
86
- allowlist_count: @author_allowlist.length)
87
93
  end
88
94
 
89
95
  def process(pr)
90
96
  number = pr[:number]
91
97
 
98
+ Aidp.log_debug(
99
+ "change_request_processor", "Starting change request processing",
100
+ pr_number: number, pr_title: pr[:title]
101
+ )
102
+
92
103
  unless @config[:enabled]
93
- display_message("ℹ️ PR change requests are disabled in configuration. Skipping PR ##{number}.", type: :muted)
104
+ display_message(
105
+ "ℹ️ PR change requests are disabled in configuration. Skipping PR ##{number}.",
106
+ type: :muted
107
+ )
94
108
  return
95
109
  end
96
110
 
97
111
  # Check clarification round limit
98
112
  existing_data = @state_store.change_request_data(number)
99
113
  if existing_data && existing_data["clarification_count"].to_i >= MAX_CLARIFICATION_ROUNDS
100
- display_message("⚠️ Max clarification rounds (#{MAX_CLARIFICATION_ROUNDS}) reached for PR ##{number}. Skipping.", type: :warn)
114
+ display_message(
115
+ "⚠️ Max clarification rounds (#{MAX_CLARIFICATION_ROUNDS}) reached for PR ##{number}. Skipping.",
116
+ type: :warn
117
+ )
101
118
  post_max_rounds_comment(pr)
102
119
  return
103
120
  end
104
121
 
105
- display_message("📝 Processing change request for PR ##{number} (#{pr[:title]})", type: :info)
122
+ display_message(
123
+ "📝 Processing change request for PR ##{number} (#{pr[:title]})",
124
+ type: :info
125
+ )
106
126
 
107
127
  # Fetch PR details
108
128
  pr_data = @repository_client.fetch_pull_request(number)
@@ -112,29 +132,135 @@ module Aidp
112
132
  authorized_comments = filter_authorized_comments(comments, pr_data)
113
133
 
114
134
  if authorized_comments.empty?
115
- display_message("ℹ️ No authorized comments found for PR ##{number}. Skipping.", type: :muted)
135
+ display_message(
136
+ "ℹ️ No authorized comments found for PR ##{number}. Skipping.",
137
+ type: :muted
138
+ )
116
139
  return
117
140
  end
118
141
 
119
- # If max_diff_size is set, attempt to fetch and check diff
120
- # But bypass restriction for worktree-based workflows
142
+ # Fetch diff to check size with enhanced strategy
121
143
  diff = @repository_client.fetch_pull_request_diff(number)
122
144
  diff_size = diff.lines.count
123
145
 
124
- # Check if we want to use the worktree bypass
125
- use_worktree_bypass = @config[:allow_large_pr_worktree_bypass] || @config[:allow_large_pr_worktree_bypass].nil?
146
+ # Enhanced diff size and worktree handling
147
+ large_pr = diff_size > @config[:max_diff_size]
148
+
149
+ if large_pr
150
+ # Comprehensive logging for large PR detection
151
+ Aidp.log_debug(
152
+ "change_request_processor", "Large PR detected",
153
+ pr_number: number,
154
+ diff_size: diff_size,
155
+ max_diff_size: @config[:max_diff_size],
156
+ large_pr_strategy: @config[:large_pr_strategy]
157
+ )
126
158
 
127
- if diff_size > @config[:max_diff_size] && !use_worktree_bypass
128
- display_message("⚠️ PR ##{number} diff too large (#{diff_size} lines > #{@config[:max_diff_size]}). Skipping.", type: :warn)
129
- post_diff_too_large_comment(pr, diff_size)
130
- return
131
- end
159
+ display_message(
160
+ "⚠️ Large PR detected - applying enhanced worktree handling strategy.",
161
+ type: :info
162
+ )
132
163
 
133
- # Log the diff size for observability
134
- 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)
164
+ # Handle different strategies for large PRs
165
+ case @config[:large_pr_strategy]
166
+ when "skip"
167
+ Aidp.log_debug(
168
+ "change_request_processor", "Skipping large PR processing",
169
+ pr_number: number
170
+ )
171
+ post_diff_too_large_comment(pr_data, diff_size)
172
+ return
173
+ when "manual"
174
+ post_diff_too_large_comment(pr_data, diff_size)
175
+ display_message("❌ Change request processing failed: Large PR requires manual processing. See comment for details.", type: :error)
176
+ @state_store.record_change_request(pr_data[:number], {
177
+ status: "manual_processing_required",
178
+ timestamp: Time.now.utc.iso8601,
179
+ diff_size: diff_size,
180
+ max_diff_size: @config[:max_diff_size]
181
+ })
182
+ raise "Large PR requires manual processing. See comment for details."
183
+ when "create_worktree"
184
+ # Use our enhanced WorktreeBranchManager to handle PR worktrees
185
+ begin
186
+ # Get PR branch information
187
+ head_ref = @worktree_branch_manager.get_pr_branch(number)
188
+
189
+ # Check if a PR-specific worktree already exists
190
+ Aidp.log_debug(
191
+ "change_request_processor", "Checking for existing PR worktree",
192
+ pr_number: number,
193
+ head_branch: head_ref
194
+ )
195
+
196
+ # Try to find existing worktree first
197
+ existing_worktree = @worktree_branch_manager.find_worktree(
198
+ branch: head_ref,
199
+ pr_number: number
200
+ )
201
+
202
+ # Create new worktree if none exists
203
+ if existing_worktree.nil?
204
+ Aidp.log_info(
205
+ "change_request_processor", "Creating PR-specific worktree",
206
+ pr_number: number,
207
+ head_branch: head_ref,
208
+ base_branch: pr_data[:base_ref],
209
+ strategy: "create_worktree"
210
+ )
211
+
212
+ # Use find_or_create_pr_worktree for PR-specific handling
213
+ @worktree_branch_manager.find_or_create_pr_worktree(
214
+ pr_number: number,
215
+ head_branch: head_ref,
216
+ base_branch: pr_data[:base_ref]
217
+ )
218
+ else
219
+ Aidp.log_debug(
220
+ "change_request_processor", "Using existing PR worktree",
221
+ pr_number: number,
222
+ worktree_path: existing_worktree
223
+ )
224
+ end
225
+ rescue => e
226
+ Aidp.log_error(
227
+ "change_request_processor", "Large PR worktree handling failed",
228
+ pr_number: number,
229
+ error: e.message,
230
+ strategy: @config[:large_pr_strategy]
231
+ )
232
+
233
+ # Fallback error handling
234
+ post_diff_too_large_comment(pr_data, diff_size)
235
+ raise "Failed to handle large PR: #{e.message}"
236
+ end
237
+ else
238
+ # Default fallback
239
+ Aidp.log_warn(
240
+ "change_request_processor", "Unknown large_pr_strategy",
241
+ strategy: @config[:large_pr_strategy],
242
+ fallback: "skip"
243
+ )
244
+ post_diff_too_large_comment(pr_data, diff_size)
245
+ return
246
+ end
247
+
248
+ # Provide additional context via debug log
249
+ Aidp.log_info(
250
+ "change_request_processor", "Large PR worktree strategy applied",
251
+ pr_number: number,
252
+ diff_size: diff_size,
253
+ max_diff_size: @config[:max_diff_size],
254
+ strategy: @config[:large_pr_strategy]
255
+ )
256
+ end
135
257
 
136
258
  # Analyze change requests
137
- analysis_result = analyze_change_requests(pr_data: pr_data, comments: authorized_comments, diff: diff)
259
+ analysis_result = analyze_change_requests(
260
+ pr_data: pr_data,
261
+ comments: authorized_comments,
262
+ diff: diff
263
+ )
138
264
 
139
265
  if analysis_result[:needs_clarification]
140
266
  handle_clarification_needed(pr: pr_data, analysis: analysis_result)
@@ -144,8 +270,17 @@ module Aidp
144
270
  handle_cannot_implement(pr: pr_data, analysis: analysis_result)
145
271
  end
146
272
  rescue => e
147
- display_message("❌ Change request processing failed: #{e.message}", type: :error)
148
- Aidp.log_error("change_request_processor", "Change request failed", pr: pr[:number], error: e.message, backtrace: e.backtrace&.first(10))
273
+ display_message(
274
+ " Change request processing failed: #{e.message}",
275
+ type: :error
276
+ )
277
+ Aidp.log_error(
278
+ "change_request_processor", "Change request failed",
279
+ pr: pr[:number],
280
+ error: e.message,
281
+ backtrace: e.backtrace&.first(10),
282
+ error_class: e.class.name
283
+ )
149
284
 
150
285
  # Record failure state internally but DON'T post error to GitHub
151
286
  # (per issue #280 - error messages should never appear on issues)
@@ -162,29 +297,15 @@ module Aidp
162
297
  def filter_authorized_comments(comments, pr_data)
163
298
  # If allowlist is empty (for private repos), consider PR author and all commenters
164
299
  # For public repos, enforce allowlist
165
- Aidp.log_debug("change_request_processor", "filtering_authorized_comments",
166
- total_comments: comments.length,
167
- allowlist_count: @author_allowlist.length,
168
- is_private_repo: @author_allowlist.empty?)
169
-
170
300
  if @author_allowlist.empty?
171
301
  # Private repo: trust all comments from PR participants
172
- Aidp.log_debug("change_request_processor", "private_repo_comments_allowed",
173
- comments_allowed: comments.length)
174
302
  comments
175
303
  else
176
304
  # Public repo: only allow comments from allowlisted users
177
- authorized_comments = comments.select do |comment|
305
+ comments.select do |comment|
178
306
  author = comment[:author]
179
307
  @author_allowlist.include?(author)
180
308
  end
181
-
182
- Aidp.log_debug("change_request_processor", "public_repo_comment_filtering",
183
- total_comments: comments.length,
184
- authorized_comments: authorized_comments.length,
185
- allowed_authors: authorized_comments.map { |c| c[:author] })
186
-
187
- authorized_comments
188
309
  end
189
310
  end
190
311
 
@@ -206,19 +327,41 @@ module Aidp
206
327
  # Parse JSON response
207
328
  parsed = JSON.parse(json_content)
208
329
 
209
- {
330
+ # Additional structured analysis
331
+ result = {
210
332
  can_implement: parsed["can_implement"],
211
333
  needs_clarification: parsed["needs_clarification"],
212
334
  clarifying_questions: parsed["clarifying_questions"] || [],
213
335
  reason: parsed["reason"],
214
- changes: parsed["changes"] || []
336
+ changes: []
215
337
  }
338
+
339
+ # Enhanced change parsing
340
+ begin
341
+ result[:changes] = parse_ai_changes(
342
+ {changes: parsed["changes"]},
343
+ pr_data,
344
+ comments
345
+ )
346
+ rescue => e
347
+ Aidp.log_warn("change_request_processor", "Change parsing failed",
348
+ pr_number: pr_data[:number],
349
+ error: e.message)
350
+ end
351
+
352
+ Aidp.log_debug("change_request_processor", "Change request analysis result",
353
+ pr_number: pr_data[:number],
354
+ can_implement: result[:can_implement],
355
+ needs_clarification: result[:needs_clarification],
356
+ changes_count: result[:changes].length)
357
+
358
+ result
216
359
  rescue JSON::ParserError => e
217
360
  Aidp.log_error("change_request_processor", "Failed to parse AI response", error: e.message, content: content)
218
- {can_implement: false, needs_clarification: false, reason: "Failed to parse AI analysis"}
361
+ {can_implement: false, needs_clarification: false, reason: "Failed to parse AI analysis", changes: []}
219
362
  rescue => e
220
363
  Aidp.log_error("change_request_processor", "AI analysis failed", error: e.message)
221
- {can_implement: false, needs_clarification: false, reason: "AI analysis error: #{e.message}"}
364
+ {can_implement: false, needs_clarification: false, reason: "AI analysis error: #{e.message}", changes: []}
222
365
  end
223
366
 
224
367
  def change_request_system_prompt
@@ -347,194 +490,351 @@ module Aidp
347
490
  end
348
491
 
349
492
  def checkout_pr_branch(pr_data)
350
- head_ref = pr_data[:head_ref]
351
493
  pr_number = pr_data[:number]
494
+ base_branch = pr_data[:base_ref]
352
495
 
353
- worktree_path = resolve_worktree_for_pr(pr_data)
354
-
355
- Dir.chdir(worktree_path) do
356
- run_git(%w[fetch origin], allow_failure: true)
357
- run_git(["checkout", head_ref])
358
- run_git(%w[pull --ff-only], allow_failure: true)
359
- end
360
-
361
- @project_dir = worktree_path
496
+ Aidp.log_debug(
497
+ "change_request_processor", "Starting PR branch checkout process",
498
+ pr_number: pr_number,
499
+ base_branch: base_branch,
500
+ project_dir: @project_dir
501
+ )
362
502
 
363
- Aidp.log_debug("change_request_processor", "Checked out PR branch", branch: head_ref, worktree: worktree_path)
364
- display_message("🌿 Using worktree for PR ##{pr_number}: #{head_ref}", type: :info)
365
- end
503
+ # Get PR branch information
504
+ head_ref = @worktree_branch_manager.get_pr_branch(pr_number)
366
505
 
367
- def resolve_worktree_for_pr(pr_data)
368
- head_ref = pr_data[:head_ref]
369
- pr_number = pr_data[:number]
506
+ begin
507
+ # Advanced worktree strategy with more detailed lookup
508
+ existing_worktree = @worktree_branch_manager.find_worktree(
509
+ branch: head_ref,
510
+ pr_number: pr_number
511
+ )
370
512
 
371
- existing = Aidp::Worktree.find_by_branch(branch: head_ref, project_dir: @project_dir)
513
+ # Logging: Detailed worktree search strategy
514
+ log_worktree_strategy = {
515
+ pr_number: pr_number,
516
+ head_branch: head_ref,
517
+ existing_worktree: !!existing_worktree,
518
+ worktree_creation_strategy: @config.fetch(:worktree_strategy, "auto")
519
+ }
372
520
 
373
- if existing && existing[:active]
374
- display_message("🔄 Using existing worktree for branch: #{head_ref}", type: :info)
375
- Aidp.log_debug("change_request_processor", "worktree_reused", pr_number: pr_number, branch: head_ref, path: existing[:path])
376
- return existing[:path]
377
- end
521
+ # Enhanced logging for worktree preparation
522
+ Aidp.log_info(
523
+ "change_request_processor", "Preparing PR worktree",
524
+ **log_worktree_strategy
525
+ )
378
526
 
379
- issue_worktree = find_issue_worktree_for_pr(pr_data)
380
- return issue_worktree if issue_worktree
527
+ # Worktree creation or reuse strategy
528
+ case @config.fetch(:worktree_strategy, "auto")
529
+ when "always_create"
530
+ # Force new worktree creation, useful for complex/large PRs
531
+ Aidp.log_info("change_request_processor", "Forcing new worktree creation", pr: pr_number)
532
+ existing_worktree = nil
533
+ when "reuse_only"
534
+ # Use only existing worktrees, error if not found
535
+ unless existing_worktree
536
+ raise "No existing worktree found for PR ##{pr_number}"
537
+ end
538
+ else # "auto" or default
539
+ # Existing default behavior: find or create
540
+ end
381
541
 
382
- create_worktree_for_pr(pr_data)
383
- end
542
+ # Use the enhanced find_or_create_pr_worktree method for PR-specific worktree handling
543
+ worktree_path = @worktree_branch_manager.find_or_create_pr_worktree(
544
+ pr_number: pr_number,
545
+ head_branch: head_ref,
546
+ base_branch: base_branch
547
+ )
384
548
 
385
- def find_issue_worktree_for_pr(pr_data)
386
- pr_number = pr_data[:number]
387
- linked_issue_numbers = extract_issue_numbers_from_pr(pr_data)
549
+ # Detailed logging of worktree path and strategy
550
+ Aidp.log_debug(
551
+ "change_request_processor", "PR worktree determined",
552
+ worktree_path: worktree_path,
553
+ creation_strategy: existing_worktree ? "reused" : "created",
554
+ pr_number: pr_number
555
+ )
388
556
 
389
- build_match = @state_store.find_build_by_pr(pr_number)
390
- linked_issue_numbers << build_match[:issue_number] if build_match
391
- linked_issue_numbers = linked_issue_numbers.compact.uniq
557
+ display_message("🔄 Using PR-specific worktree for branch: #{head_ref}", type: :info)
558
+
559
+ # Update project directory to use the worktree
560
+ @project_dir = worktree_path
561
+
562
+ # Ensure the branch is up-to-date with remote
563
+ Dir.chdir(@project_dir) do
564
+ run_git(["fetch", "origin", base_branch], allow_failure: true)
565
+ run_git(["fetch", "origin", head_ref], allow_failure: true)
566
+
567
+ # Checkout branch with more detailed tracking
568
+ checkout_result = run_git(["checkout", head_ref])
569
+
570
+ # Pull with fast-forward preference for cleaner history
571
+ pull_result = run_git(
572
+ ["pull", "--ff-only", "origin", head_ref],
573
+ allow_failure: true
574
+ )
575
+
576
+ # Enhanced branch state logging
577
+ Aidp.log_info(
578
+ "change_request_processor", "Branch synchronization complete",
579
+ pr_number: pr_number,
580
+ checkout_status: checkout_result.strip,
581
+ pull_status: pull_result.strip,
582
+ current_branch: run_git(["rev-parse", "--abbrev-ref", "HEAD"]).strip
583
+ )
584
+ end
392
585
 
393
- linked_issue_numbers.each do |issue_number|
394
- workstream = @state_store.workstream_for_issue(issue_number)
395
- next unless workstream
586
+ # Additional validation and logging
587
+ worktree_details = {
588
+ path: worktree_path,
589
+ branch: head_ref,
590
+ base_branch: base_branch,
591
+ pr_number: pr_number,
592
+ timestamp: Time.now.utc.iso8601
593
+ }
396
594
 
397
- slug = workstream[:workstream]
398
- branch = workstream[:branch]
595
+ Aidp.log_debug(
596
+ "change_request_processor", "Validated PR worktree",
597
+ **worktree_details
598
+ )
399
599
 
400
- if slug
401
- info = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
402
- if info && info[:active]
403
- Aidp.log_debug("change_request_processor", "issue_worktree_reused", pr_number: pr_number, issue_number: issue_number, branch: branch, path: info[:path])
404
- display_message("🔄 Reusing worktree #{slug} for issue ##{issue_number} (PR ##{pr_number})", type: :info)
405
- return info[:path]
406
- end
407
- end
600
+ worktree_path
601
+ rescue => e
602
+ Aidp.log_error(
603
+ "change_request_processor", "Critical worktree preparation failure",
604
+ pr_number: pr_number,
605
+ base_branch: base_branch,
606
+ head_branch: head_ref,
607
+ error: e.message,
608
+ error_class: e.class.name,
609
+ backtrace: e.backtrace.first(10)
610
+ )
408
611
 
409
- if branch
410
- existing = Aidp::Worktree.find_by_branch(branch: branch, project_dir: @project_dir)
411
- if existing && existing[:active]
412
- Aidp.log_debug("change_request_processor", "issue_branch_worktree_reused", pr_number: pr_number, issue_number: issue_number, branch: branch, path: existing[:path])
413
- display_message("🔄 Reusing branch worktree for issue ##{issue_number}: #{branch}", type: :info)
414
- return existing[:path]
415
- end
416
- end
612
+ # Provide detailed error handling and recovery
613
+ handle_worktree_error(pr_data, e)
417
614
  end
418
-
419
- nil
420
615
  end
421
616
 
422
- def extract_issue_numbers_from_pr(pr_data)
423
- body = pr_data[:body].to_s
424
- issue_matches = body.scan(/(?:Fixes|Resolves|Closes)\s+#(\d+)/i).flatten
617
+ def handle_worktree_error(pr_data, error)
618
+ # Log the detailed error
619
+ Aidp.log_error(
620
+ "change_request_processor", "Worktree preparation critical error",
621
+ pr_number: pr_data[:number],
622
+ error_message: error.message,
623
+ error_class: error.class.name
624
+ )
425
625
 
426
- issue_matches.map { |num| num.to_i }.uniq
427
- end
626
+ # Post a comment to GitHub about the failure
627
+ comment_body = <<~COMMENT
628
+ #{COMMENT_HEADER}
428
629
 
429
- def create_worktree_for_pr(pr_data)
430
- head_ref = pr_data[:head_ref]
431
- pr_number = pr_data[:number]
630
+ Automated worktree preparation failed for this pull request.
432
631
 
433
- # Configure slug and worktree strategy
434
- slug = pr_data.fetch(:worktree_slug, "pr-#{pr_number}-change-requests")
435
- strategy = @config.fetch(:worktree_strategy, "auto")
632
+ **Error Details:**
633
+ ```
634
+ #{error.message}
635
+ ```
436
636
 
437
- display_message("🌿 Preparing worktree for PR ##{pr_number}: #{head_ref} (Strategy: #{strategy})", type: :info)
637
+ **Possible Actions:**
638
+ 1. Review the PR branch and its configuration
639
+ 2. Check if the base repository is accessible
640
+ 3. Try re-adding the `#{@change_request_label}` label
438
641
 
439
- # Pre-create setup: fetch latest refs
440
- Dir.chdir(@project_dir) do
441
- run_git(%w[fetch origin], allow_failure: true)
442
- end
642
+ This may require manual intervention or administrative access.
643
+ COMMENT
443
644
 
444
- # Worktree creation strategy
445
- worktree_path =
446
- case strategy
447
- when "always_create"
448
- create_fresh_worktree(pr_data, slug)
449
- when "reuse_only"
450
- find_existing_worktree(pr_data, slug)
451
- else # 'auto' or default
452
- find_existing_worktree(pr_data, slug) || create_fresh_worktree(pr_data, slug)
453
- end
645
+ begin
646
+ @repository_client.post_comment(pr_data[:number], comment_body)
454
647
 
455
- Aidp.log_debug(
456
- "change_request_processor",
457
- "worktree_resolved",
458
- pr_number: pr_number,
459
- branch: head_ref,
460
- path: worktree_path,
461
- strategy: strategy
462
- )
648
+ # Optionally remove or modify the label to indicate a problem
649
+ @repository_client.replace_labels(
650
+ pr_data[:number],
651
+ old_labels: [@change_request_label],
652
+ new_labels: ["aidp-worktree-error"]
653
+ )
654
+ rescue => comment_error
655
+ Aidp.log_warn(
656
+ "change_request_processor", "Failed to post error comment",
657
+ pr_number: pr_data[:number],
658
+ error: comment_error.message
659
+ )
660
+ end
463
661
 
464
- display_message("✅ Worktree available at #{worktree_path}", type: :success)
465
- worktree_path
466
- rescue => e
467
- Aidp.log_error(
468
- "change_request_processor",
469
- "worktree_creation_failed",
470
- pr_number: pr_number,
471
- error: e.message,
472
- backtrace: e.backtrace&.first(5)
473
- )
474
- display_message("❌ Failed to create worktree: #{e.message}", type: :error)
475
- raise
662
+ # Re-raise the original error to halt processing
663
+ raise error
476
664
  end
477
665
 
478
666
  private
479
667
 
480
- def find_existing_worktree(pr_data, slug)
481
- head_ref = pr_data[:head_ref]
482
- pr_number = pr_data[:number]
483
-
484
- # First check for existing worktree by branch
485
- existing = Aidp::Worktree.find_by_branch(branch: head_ref, project_dir: @project_dir)
486
- return existing[:path] if existing && existing[:active]
668
+ def apply_changes(changes)
669
+ # Validate we're in a worktree for enhanced change handling
670
+ in_worktree = @project_dir.include?(".worktrees")
671
+
672
+ Aidp.log_debug("change_request_processor", "Starting change application",
673
+ total_changes: changes.length,
674
+ in_worktree: in_worktree,
675
+ working_dir: @project_dir)
676
+
677
+ # Track overall change application results
678
+ results = {
679
+ total_changes: changes.length,
680
+ successful_changes: 0,
681
+ failed_changes: 0,
682
+ skipped_changes: 0,
683
+ errors: [],
684
+ worktree_context: in_worktree
685
+ }
487
686
 
488
- # If no branch-specific worktree, look for PR-specific worktree
489
- pr_worktrees = Aidp::Worktree.list(project_dir: @project_dir)
490
- pr_specific_worktree = pr_worktrees.find do |w|
491
- w[:slug]&.include?("pr-#{pr_number}")
492
- end
687
+ # Enhanced change application with worktree context awareness
688
+ changes.each_with_index do |change, index|
689
+ file_path = change["file"].start_with?(@project_dir) ? change["file"] : File.join(@project_dir, change["file"])
493
690
 
494
- pr_specific_worktree ? pr_specific_worktree[:path] : nil
495
- end
691
+ Aidp.log_debug("change_request_processor", "Preparing to apply change",
692
+ index: index + 1,
693
+ total: changes.length,
694
+ action: change["action"],
695
+ file: change["file"],
696
+ content_length: change["content"]&.length || 0,
697
+ in_worktree: in_worktree)
496
698
 
497
- def create_fresh_worktree(pr_data, slug)
498
- head_ref = pr_data[:head_ref]
499
- pr_number = pr_data[:number]
699
+ begin
700
+ case change["action"]
701
+ when "create", "edit"
702
+ # Enhanced file change strategy with worktree awareness
703
+ unless change["content"]
704
+ results[:skipped_changes] += 1
705
+ Aidp.log_warn("change_request_processor", "Skipping change with empty content",
706
+ file: change["file"], action: change["action"], in_worktree: in_worktree)
707
+ next
708
+ end
709
+
710
+ # Enhanced directory creation with worktree validation
711
+ target_directory = File.dirname(file_path)
712
+ unless File.directory?(target_directory)
713
+ FileUtils.mkdir_p(target_directory)
714
+ Aidp.log_debug("change_request_processor", "Created directory structure",
715
+ directory: target_directory, in_worktree: in_worktree)
716
+ end
717
+
718
+ # Preserve file permissions if file already exists
719
+ old_permissions = File.exist?(file_path) ? File.stat(file_path).mode : 0o644
720
+
721
+ # Enhanced content writing with validation
722
+ File.write(file_path, change["content"])
723
+ File.chmod(old_permissions, file_path)
724
+
725
+ # Validate file was written correctly
726
+ unless File.exist?(file_path) && File.readable?(file_path)
727
+ raise "File creation validation failed"
728
+ end
729
+
730
+ display_message(" ✓ #{change["action"]} #{change["file"]}", type: :muted) if @verbose
731
+ Aidp.log_debug("change_request_processor", "Applied file change",
732
+ action: change["action"],
733
+ file: change["file"],
734
+ content_preview: change["content"]&.slice(0, 100),
735
+ file_size: File.size(file_path),
736
+ original_source_comments: change["source_comment_urls"],
737
+ in_worktree: in_worktree)
738
+
739
+ results[:successful_changes] += 1
740
+
741
+ when "delete"
742
+ # Enhanced delete strategy with worktree context
743
+ if File.exist?(file_path)
744
+ # Additional validation for worktree safety
745
+ if in_worktree
746
+ canonical_path = File.expand_path(file_path)
747
+ worktree_canonical = File.expand_path(@project_dir)
748
+ unless canonical_path.start_with?(worktree_canonical)
749
+ raise SecurityError, "Attempted to delete file outside worktree"
750
+ end
751
+ end
752
+
753
+ File.delete(file_path)
754
+ display_message(" ✓ Deleted #{change["file"]}", type: :muted) if @verbose
755
+ Aidp.log_debug("change_request_processor", "Deleted file",
756
+ file: change["file"],
757
+ original_source_comments: change["source_comment_urls"],
758
+ in_worktree: in_worktree)
759
+ results[:successful_changes] += 1
760
+ else
761
+ results[:skipped_changes] += 1
762
+ Aidp.log_warn("change_request_processor", "File to delete does not exist",
763
+ file: change["file"], in_worktree: in_worktree)
764
+ end
765
+
766
+ else
767
+ results[:skipped_changes] += 1
768
+ error_msg = "Unknown change action: #{change["action"]}"
769
+ display_message(" ⚠️ #{error_msg} for #{change["file"]}", type: :warn)
770
+
771
+ results[:errors] << {
772
+ file: change["file"],
773
+ action: change["action"],
774
+ error: error_msg,
775
+ worktree_context: in_worktree
776
+ }
500
777
 
501
- Aidp.log_debug(
502
- "change_request_processor",
503
- "creating_new_worktree",
504
- pr_number: pr_number,
505
- branch: head_ref,
506
- slug: slug
507
- )
778
+ Aidp.log_warn("change_request_processor", "Unhandled change action",
779
+ action: change["action"],
780
+ file: change["file"],
781
+ in_worktree: in_worktree)
782
+ end
783
+ rescue SecurityError => e
784
+ results[:failed_changes] += 1
785
+ error_details = {
786
+ file: change["file"],
787
+ action: change["action"],
788
+ error: e.message,
789
+ error_type: "security_violation",
790
+ worktree_context: in_worktree
791
+ }
792
+
793
+ results[:errors] << error_details
794
+
795
+ Aidp.log_error("change_request_processor", "Security violation during change application",
796
+ **error_details)
797
+
798
+ display_message(" 🔒 Security error applying change to #{change["file"]}: #{e.message}", type: :error)
799
+ rescue => e
800
+ results[:failed_changes] += 1
801
+ error_details = {
802
+ file: change["file"],
803
+ action: change["action"],
804
+ error: e.message,
805
+ backtrace: e.backtrace&.first(3),
806
+ worktree_context: in_worktree
807
+ }
508
808
 
509
- result = Aidp::Worktree.create(
510
- slug: slug,
511
- project_dir: @project_dir,
512
- branch: head_ref,
513
- base_branch: pr_data[:base_ref]
514
- )
809
+ results[:errors] << error_details
515
810
 
516
- result[:path]
517
- end
811
+ Aidp.log_error("change_request_processor", "Change application failed",
812
+ **error_details)
518
813
 
519
- def apply_changes(changes)
520
- changes.each do |change|
521
- file_path = File.join(@project_dir, change["file"])
522
-
523
- case change["action"]
524
- when "create", "edit"
525
- FileUtils.mkdir_p(File.dirname(file_path))
526
- File.write(file_path, change["content"])
527
- display_message(" ✓ #{change["action"]} #{change["file"]}", type: :muted) if @verbose
528
- Aidp.log_debug("change_request_processor", "Applied change", action: change["action"], file: change["file"])
529
- when "delete"
530
- File.delete(file_path) if File.exist?(file_path)
531
- display_message(" ✓ Deleted #{change["file"]}", type: :muted) if @verbose
532
- Aidp.log_debug("change_request_processor", "Deleted file", file: change["file"])
533
- else
534
- display_message(" ⚠️ Unknown action: #{change["action"]} for #{change["file"]}", type: :warn)
535
- Aidp.log_warn("change_request_processor", "Unknown change action", action: change["action"], file: change["file"])
814
+ display_message(" ❌ Failed to apply change to #{change["file"]}: #{e.message}", type: :error)
536
815
  end
537
816
  end
817
+
818
+ # Enhanced logging of overall change application results
819
+ Aidp.log_info("change_request_processor", "Change application summary",
820
+ total_changes: results[:total_changes],
821
+ successful_changes: results[:successful_changes],
822
+ skipped_changes: results[:skipped_changes],
823
+ failed_changes: results[:failed_changes],
824
+ errors_count: results[:errors].length,
825
+ success_rate: (results[:successful_changes].to_f / results[:total_changes] * 100).round(2),
826
+ in_worktree: in_worktree,
827
+ working_directory: @project_dir)
828
+
829
+ # Additional worktree-specific logging
830
+ if in_worktree && results[:successful_changes] > 0
831
+ Aidp.log_info("change_request_processor", "Worktree changes applied successfully",
832
+ worktree_path: @project_dir,
833
+ files_modified: results[:successful_changes])
834
+ end
835
+
836
+ # Return enhanced results for potential additional handling
837
+ results
538
838
  end
539
839
 
540
840
  def run_tests_and_linters
@@ -565,30 +865,96 @@ module Aidp
565
865
 
566
866
  def commit_and_push(pr_data, analysis)
567
867
  Dir.chdir(@project_dir) do
568
- # Check if there are changes
569
- status_output = run_git(%w[status --porcelain])
570
- if status_output.strip.empty?
571
- display_message("ℹ️ No changes to commit after applying changes.", type: :muted)
868
+ # Validate we're in a worktree
869
+ unless @project_dir.include?(".worktrees")
870
+ Aidp.log_error(
871
+ "change_request_processor", "Invalid project directory for commit_and_push",
872
+ pr_number: pr_data[:number],
873
+ project_dir: @project_dir
874
+ )
572
875
  return false
573
876
  end
574
877
 
575
- # Stage all changes
576
- run_git(%w[add -A])
577
-
578
- # Create commit
579
- commit_message = build_commit_message(pr_data, analysis)
580
- run_git(["commit", "-m", commit_message])
581
-
582
- display_message("💾 Created commit: #{commit_message.lines.first.strip}", type: :info)
583
- Aidp.log_debug("change_request_processor", "Created commit", pr: pr_data[:number])
584
-
585
- # Push to origin
586
- head_ref = pr_data[:head_ref]
587
- run_git(["push", "origin", head_ref])
588
-
589
- display_message("⬆️ Pushed changes to #{head_ref}", type: :success)
590
- Aidp.log_info("change_request_processor", "Pushed changes", pr: pr_data[:number], branch: head_ref)
591
- true
878
+ begin
879
+ # Check for changes
880
+ status_result = run_git(%w[status --porcelain])
881
+ return false if status_result.strip.empty?
882
+
883
+ modified_files = status_result.split("\n").map { |l| l.strip.split(" ", 2).last }
884
+
885
+ # Stage all changes
886
+ run_git(%w[add -A])
887
+ Aidp.log_debug("change_request_processor", "Staged changes",
888
+ pr_number: pr_data[:number],
889
+ staged_files: modified_files)
890
+
891
+ # Create commit
892
+ commit_message = build_commit_message(pr_data, analysis)
893
+ commit_result = run_git(["commit", "-m", commit_message])
894
+ first_line = commit_message.lines.first.strip
895
+ Aidp.log_info(
896
+ "change_request_processor", "Created commit",
897
+ pr: pr_data[:number],
898
+ commit_message: first_line,
899
+ files_changed: modified_files,
900
+ commit_result: commit_result.strip
901
+ )
902
+ display_message("💾 Created commit: #{first_line}", type: :info)
903
+
904
+ # Push changes
905
+ head_ref = pr_data[:head_ref]
906
+ begin
907
+ push_result = run_git(["push", "origin", head_ref], allow_failure: false)
908
+ Aidp.log_info("change_request_processor", "Pushed changes",
909
+ pr: pr_data[:number],
910
+ branch: head_ref,
911
+ push_result: push_result.strip)
912
+ display_message("⬆️ Pushed changes to #{head_ref}", type: :success)
913
+ return true
914
+ rescue => push_error
915
+ # Push failed, but commit was successful
916
+ Aidp.log_warn("change_request_processor", "Push failed, but commit was successful",
917
+ pr_number: pr_data[:number],
918
+ error: push_error.message)
919
+
920
+ # Post a detailed comment about the push failure
921
+ comment_body = <<~COMMENT
922
+ #{COMMENT_HEADER}
923
+
924
+ ⚠️ Automated changes were committed successfully, but pushing to the branch failed.
925
+
926
+ **Error Details:**
927
+ ```
928
+ #{push_error.message}
929
+ ```
930
+
931
+ **Suggested Actions:**
932
+ 1. Check branch permissions
933
+ 2. Verify remote repository configuration
934
+ 3. Manually push the changes
935
+ 4. Contact repository administrator
936
+ COMMENT
937
+
938
+ begin
939
+ @repository_client.post_comment(pr_data[:number], comment_body)
940
+ rescue => comment_error
941
+ Aidp.log_warn("change_request_processor", "Failed to post push error comment",
942
+ pr_number: pr_data[:number],
943
+ error: comment_error.message)
944
+ end
945
+
946
+ return false
947
+ end
948
+ rescue => error
949
+ # Catch any other unexpected errors during the entire process
950
+ Aidp.log_error(
951
+ "change_request_processor", "Unexpected error in commit_and_push",
952
+ pr_number: pr_data[:number],
953
+ error: error.message,
954
+ backtrace: error.backtrace.first(5)
955
+ )
956
+ return false
957
+ end
592
958
  end
593
959
  end
594
960
 
@@ -670,42 +1036,22 @@ module Aidp
670
1036
  end
671
1037
 
672
1038
  def handle_incomplete_implementation(pr:, analysis:, verification_result:)
673
- Aidp.log_debug("change_request_processor", "start_incomplete_implementation_handling",
674
- pr_number: pr[:number],
675
- verification_result: {
676
- missing_items_count: verification_result[:missing_items]&.length || 0,
677
- additional_work_count: verification_result[:additional_work]&.length || 0
678
- })
679
-
680
1039
  display_message("⚠️ Implementation incomplete; creating follow-up tasks.", type: :warn)
681
1040
 
682
1041
  # Create tasks for missing requirements
683
1042
  if verification_result[:additional_work] && !verification_result[:additional_work].empty?
684
- Aidp.log_debug("change_request_processor", "preparing_follow_up_tasks",
685
- pr_number: pr[:number],
686
- additional_work_tasks_count: verification_result[:additional_work].length)
687
1043
  create_follow_up_tasks(@project_dir, verification_result[:additional_work])
688
1044
  end
689
1045
 
690
1046
  # Record state but do not post a separate comment
691
1047
  # (verification details will be included in the next summary comment)
692
- state_record = {
1048
+ @state_store.record_change_request(pr[:number], {
693
1049
  status: "incomplete_implementation",
694
1050
  timestamp: Time.now.utc.iso8601,
695
1051
  verification_reasons: verification_result[:reasons],
696
1052
  missing_items: verification_result[:missing_items],
697
1053
  additional_work: verification_result[:additional_work]
698
- }
699
-
700
- # Log the details of the state record before storing
701
- Aidp.log_debug("change_request_processor", "recording_incomplete_implementation_state",
702
- pr_number: pr[:number],
703
- status: state_record[:status],
704
- verification_reasons_count: state_record[:verification_reasons]&.length || 0,
705
- missing_items_count: state_record[:missing_items]&.length || 0,
706
- additional_work_count: state_record[:additional_work]&.length || 0)
707
-
708
- @state_store.record_change_request(pr[:number], state_record)
1054
+ })
709
1055
 
710
1056
  display_message("📝 Recorded incomplete implementation status for PR ##{pr[:number]}", type: :info)
711
1057
 
@@ -722,55 +1068,36 @@ module Aidp
722
1068
  def create_follow_up_tasks(working_dir, additional_work)
723
1069
  return if additional_work.nil? || additional_work.empty?
724
1070
 
725
- Aidp.log_debug("change_request_processor", "start_creating_follow_up_tasks",
726
- working_dir: working_dir,
727
- additional_work_tasks_count: additional_work.length)
728
-
729
1071
  tasklist_file = File.join(working_dir, ".aidp", "tasklist.jsonl")
730
1072
  FileUtils.mkdir_p(File.dirname(tasklist_file))
731
1073
 
732
1074
  require_relative "../execute/persistent_tasklist"
733
1075
  tasklist = Aidp::Execute::PersistentTasklist.new(working_dir)
734
1076
 
735
- tasks_created = []
736
1077
  additional_work.each do |task_description|
737
- task = tasklist.create(
1078
+ tasklist.create(
738
1079
  description: task_description,
739
1080
  priority: :high,
740
1081
  source: "verification"
741
1082
  )
742
- tasks_created << task
743
1083
  end
744
1084
 
745
1085
  display_message("📝 Created #{additional_work.length} follow-up task(s) for continued work", type: :info)
746
1086
 
747
- Aidp.log_debug("change_request_processor", "follow_up_tasks_details",
748
- task_count: tasks_created.length,
749
- working_dir: working_dir,
750
- task_descriptions: tasks_created.map(&:description))
751
-
752
1087
  Aidp.log_info(
753
1088
  "change_request_processor",
754
1089
  "created_follow_up_tasks",
755
- task_count: tasks_created.length,
1090
+ task_count: additional_work.length,
756
1091
  working_dir: working_dir
757
1092
  )
758
-
759
- tasks_created
760
1093
  rescue => e
1094
+ display_message("⚠️ Failed to create follow-up tasks: #{e.message}", type: :warn)
761
1095
  Aidp.log_error(
762
1096
  "change_request_processor",
763
1097
  "failed_to_create_follow_up_tasks",
764
1098
  error: e.message,
765
- error_class: e.class.name,
766
- backtrace: e.backtrace&.first(5),
767
- working_dir: working_dir
1099
+ backtrace: e.backtrace&.first(5)
768
1100
  )
769
-
770
- display_message("⚠️ Failed to create follow-up tasks: #{e.message}", type: :warn)
771
-
772
- # Return an empty array to indicate failure
773
- []
774
1101
  end
775
1102
 
776
1103
  def handle_clarification_needed(pr:, analysis:)
@@ -912,36 +1239,56 @@ module Aidp
912
1239
  end
913
1240
 
914
1241
  def post_diff_too_large_comment(pr, diff_size)
1242
+ # Configure handling based on repository/project config
1243
+ handling_strategy = case @config[:large_pr_strategy]
1244
+ when "create_worktree"
1245
+ "Creating a dedicated git worktree"
1246
+ when "manual"
1247
+ "Requiring manual intervention"
1248
+ else
1249
+ "Skipping processing"
1250
+ end
1251
+
915
1252
  comment = <<~COMMENT
916
1253
  #{COMMENT_HEADER}
917
1254
 
918
- ⚠️ PR diff is too large for default change requests.
1255
+ ⚠️ PR diff is too large for standard automated change requests.
919
1256
 
920
1257
  **Current size:** #{diff_size} lines
921
1258
  **Maximum allowed:** #{@config[:max_diff_size]} lines
1259
+ **Handling strategy:** #{handling_strategy}
922
1260
 
923
- For large PRs, you have several options:
924
- 1. Enable worktree-based large PR handling:
925
- Set `allow_large_pr_worktree_bypass: true` in your `aidp.yml`
926
- 2. Break the PR into smaller chunks
927
- 3. Implement changes manually
928
- 4. Increase `max_diff_size` in your configuration
929
-
930
- The worktree bypass allows processing large PRs by working directly in the branch
931
- instead of using diff-based changes.
1261
+ Options:
1262
+ 1. Break the PR into smaller chunks
1263
+ 2. Implement changes manually
1264
+ 3. Increase `max_diff_size` in your `aidp.yml` configuration
1265
+ 4. Configure `large_pr_strategy` to customize processing
932
1266
  COMMENT
933
1267
 
1268
+ Aidp.log_debug(
1269
+ "change_request_processor", "Large PR detected",
1270
+ pr_number: pr[:number],
1271
+ diff_size: diff_size,
1272
+ max_diff_size: @config[:max_diff_size],
1273
+ strategy: handling_strategy
1274
+ )
1275
+
934
1276
  begin
935
1277
  @repository_client.post_comment(pr[:number], comment)
936
1278
  @repository_client.remove_labels(pr[:number], @change_request_label)
937
- rescue
938
- nil
1279
+ rescue => e
1280
+ Aidp.log_warn(
1281
+ "change_request_processor", "Failed to post large PR comment",
1282
+ pr_number: pr[:number],
1283
+ error: e.message
1284
+ )
939
1285
  end
940
1286
  end
941
1287
 
942
1288
  def run_git(args, allow_failure: false)
943
- stdout, stderr, status = Open3.capture3("git", *Array(args))
944
- raise "git #{args.join(" ")} failed: #{stderr.strip}" unless status.success? || allow_failure
1289
+ args_array = Array(args)
1290
+ stdout, stderr, status = Open3.capture3("git", *args_array)
1291
+ raise "git #{args_array.join(" ")} failed: #{stderr.strip}" unless status.success? || allow_failure
945
1292
  stdout
946
1293
  end
947
1294
 
@@ -977,6 +1324,53 @@ module Aidp
977
1324
  end
978
1325
  end
979
1326
 
1327
+ def parse_ai_changes(ai_response, pr_data, comments)
1328
+ changes = ai_response[:changes] || []
1329
+
1330
+ # Log raw changes for debugging
1331
+ Aidp.log_debug(
1332
+ "change_request_processor", "Extracted changes",
1333
+ pr_number: pr_data[:number],
1334
+ changes_count: changes.length
1335
+ )
1336
+
1337
+ # Validate and sanitize changes
1338
+ validated_changes = changes.map do |change|
1339
+ # Sanitize file path
1340
+ file_path = change["file"].to_s.gsub(%r{^/|\.\.}, "")
1341
+ file_path = File.join(@project_dir, file_path) unless file_path.start_with?(@project_dir)
1342
+
1343
+ # Validate change structure
1344
+ {
1345
+ "file" => file_path,
1346
+ "action" => %w[create edit delete].include?(change["action"]) ? change["action"] : "edit",
1347
+ "content" => change["content"].to_s,
1348
+ "description" => change["description"].to_s.slice(0, 500), # Limit description length
1349
+ "line_start" => change["line_start"]&.to_i,
1350
+ "line_end" => change["line_end"]&.to_i
1351
+ }
1352
+ end.select do |change|
1353
+ # Filter out invalid or empty changes
1354
+ change["file"].present? &&
1355
+ (change["action"] == "delete" || change["content"].present?)
1356
+ end
1357
+
1358
+ # Add source reference for traceability
1359
+ validated_changes.each do |change|
1360
+ change["source_comment_urls"] = comments
1361
+ .select { |c| c[:body].include?(change["description"]) }
1362
+ .map { |c| c[:url] }
1363
+ end
1364
+
1365
+ Aidp.log_debug(
1366
+ "change_request_processor", "Validated changes",
1367
+ pr_number: pr_data[:number],
1368
+ validated_changes_count: validated_changes.length
1369
+ )
1370
+
1371
+ validated_changes
1372
+ end
1373
+
980
1374
  def symbolize_keys(hash)
981
1375
  return {} unless hash
982
1376