aidp 0.32.0 → 0.34.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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/lib/aidp/analyze/feature_analyzer.rb +322 -320
  4. data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
  5. data/lib/aidp/auto_update/coordinator.rb +97 -7
  6. data/lib/aidp/auto_update.rb +0 -12
  7. data/lib/aidp/cli/devcontainer_commands.rb +0 -5
  8. data/lib/aidp/cli/eval_command.rb +399 -0
  9. data/lib/aidp/cli/harness_command.rb +1 -1
  10. data/lib/aidp/cli/security_command.rb +416 -0
  11. data/lib/aidp/cli/tools_command.rb +6 -4
  12. data/lib/aidp/cli.rb +172 -4
  13. data/lib/aidp/comment_consolidator.rb +78 -0
  14. data/lib/aidp/concurrency/exec.rb +3 -0
  15. data/lib/aidp/concurrency.rb +0 -3
  16. data/lib/aidp/config.rb +113 -1
  17. data/lib/aidp/config_paths.rb +91 -0
  18. data/lib/aidp/daemon/runner.rb +8 -4
  19. data/lib/aidp/errors.rb +134 -0
  20. data/lib/aidp/evaluations/context_capture.rb +205 -0
  21. data/lib/aidp/evaluations/evaluation_record.rb +114 -0
  22. data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
  23. data/lib/aidp/evaluations.rb +23 -0
  24. data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
  25. data/lib/aidp/execute/interactive_repl.rb +6 -2
  26. data/lib/aidp/execute/prompt_evaluator.rb +359 -0
  27. data/lib/aidp/execute/repl_macros.rb +100 -1
  28. data/lib/aidp/execute/work_loop_runner.rb +719 -58
  29. data/lib/aidp/execute/work_loop_state.rb +4 -1
  30. data/lib/aidp/execute/workflow_selector.rb +3 -0
  31. data/lib/aidp/harness/ai_decision_engine.rb +79 -0
  32. data/lib/aidp/harness/ai_filter_factory.rb +285 -0
  33. data/lib/aidp/harness/capability_registry.rb +2 -0
  34. data/lib/aidp/harness/condition_detector.rb +3 -0
  35. data/lib/aidp/harness/config_loader.rb +3 -0
  36. data/lib/aidp/harness/config_schema.rb +97 -1
  37. data/lib/aidp/harness/config_validator.rb +1 -1
  38. data/lib/aidp/harness/configuration.rb +61 -5
  39. data/lib/aidp/harness/enhanced_runner.rb +14 -11
  40. data/lib/aidp/harness/error_handler.rb +3 -0
  41. data/lib/aidp/harness/filter_definition.rb +212 -0
  42. data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
  43. data/lib/aidp/harness/output_filter.rb +50 -25
  44. data/lib/aidp/harness/output_filter_config.rb +129 -0
  45. data/lib/aidp/harness/provider_factory.rb +3 -0
  46. data/lib/aidp/harness/provider_manager.rb +96 -2
  47. data/lib/aidp/harness/runner.rb +5 -12
  48. data/lib/aidp/harness/state/persistence.rb +3 -0
  49. data/lib/aidp/harness/state_manager.rb +3 -0
  50. data/lib/aidp/harness/status_display.rb +28 -20
  51. data/lib/aidp/harness/test_runner.rb +179 -41
  52. data/lib/aidp/harness/thinking_depth_manager.rb +44 -28
  53. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
  54. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
  55. data/lib/aidp/harness/ui/error_handler.rb +3 -0
  56. data/lib/aidp/harness/ui/job_monitor.rb +4 -0
  57. data/lib/aidp/harness/ui/navigation/submenu.rb +2 -2
  58. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
  59. data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
  60. data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
  61. data/lib/aidp/harness/user_interface.rb +3 -0
  62. data/lib/aidp/loader.rb +195 -0
  63. data/lib/aidp/logger.rb +3 -0
  64. data/lib/aidp/message_display.rb +31 -0
  65. data/lib/aidp/metadata/compiler.rb +29 -17
  66. data/lib/aidp/metadata/query.rb +1 -1
  67. data/lib/aidp/metadata/scanner.rb +8 -1
  68. data/lib/aidp/metadata/tool_metadata.rb +13 -13
  69. data/lib/aidp/metadata/validator.rb +10 -0
  70. data/lib/aidp/metadata.rb +16 -0
  71. data/lib/aidp/pr_worktree_manager.rb +20 -8
  72. data/lib/aidp/provider_manager.rb +4 -7
  73. data/lib/aidp/providers/base.rb +2 -0
  74. data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
  75. data/lib/aidp/security/secrets_proxy.rb +328 -0
  76. data/lib/aidp/security/secrets_registry.rb +227 -0
  77. data/lib/aidp/security/trifecta_state.rb +220 -0
  78. data/lib/aidp/security/watch_mode_handler.rb +306 -0
  79. data/lib/aidp/security/work_loop_adapter.rb +277 -0
  80. data/lib/aidp/security.rb +56 -0
  81. data/lib/aidp/setup/wizard.rb +283 -11
  82. data/lib/aidp/skills.rb +0 -5
  83. data/lib/aidp/storage/csv_storage.rb +3 -0
  84. data/lib/aidp/style_guide/selector.rb +360 -0
  85. data/lib/aidp/tooling_detector.rb +283 -16
  86. data/lib/aidp/version.rb +1 -1
  87. data/lib/aidp/watch/auto_merger.rb +274 -0
  88. data/lib/aidp/watch/auto_pr_processor.rb +125 -7
  89. data/lib/aidp/watch/build_processor.rb +16 -1
  90. data/lib/aidp/watch/change_request_processor.rb +682 -150
  91. data/lib/aidp/watch/ci_fix_processor.rb +262 -4
  92. data/lib/aidp/watch/feedback_collector.rb +191 -0
  93. data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
  94. data/lib/aidp/watch/implementation_verifier.rb +142 -1
  95. data/lib/aidp/watch/plan_generator.rb +70 -13
  96. data/lib/aidp/watch/plan_processor.rb +12 -5
  97. data/lib/aidp/watch/projects_processor.rb +286 -0
  98. data/lib/aidp/watch/repository_client.rb +871 -22
  99. data/lib/aidp/watch/review_processor.rb +33 -6
  100. data/lib/aidp/watch/runner.rb +80 -29
  101. data/lib/aidp/watch/state_store.rb +233 -0
  102. data/lib/aidp/watch/sub_issue_creator.rb +221 -0
  103. data/lib/aidp/watch.rb +5 -7
  104. data/lib/aidp/workflows/guided_agent.rb +4 -0
  105. data/lib/aidp/workstream_cleanup.rb +0 -2
  106. data/lib/aidp/workstream_executor.rb +3 -4
  107. data/lib/aidp/worktree.rb +61 -12
  108. data/lib/aidp/worktree_branch_manager.rb +347 -101
  109. data/lib/aidp.rb +21 -106
  110. data/templates/implementation/iterative_implementation.md +46 -3
  111. metadata +91 -36
  112. data/lib/aidp/config/paths.rb +0 -131
@@ -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,6 +45,10 @@ module Aidp
40
45
  @project_dir = project_dir
41
46
  @verbose = verbose
42
47
 
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)
51
+
43
52
  # Initialize verifier
44
53
  @verifier = ImplementationVerifier.new(
45
54
  repository_client: repository_client,
@@ -58,9 +67,26 @@ module Aidp
58
67
  commit_message_prefix: "aidp: pr-change",
59
68
  require_comment_reference: true,
60
69
  max_diff_size: 2000,
61
- 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
62
75
  }.merge(symbolize_keys(change_request_config))
63
76
 
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
+ )
89
+
64
90
  # Load safety configuration
65
91
  @safety_config = safety_config
66
92
  @author_allowlist = Array(@safety_config[:author_allowlist] || @safety_config["author_allowlist"])
@@ -69,20 +95,34 @@ module Aidp
69
95
  def process(pr)
70
96
  number = pr[:number]
71
97
 
98
+ Aidp.log_debug(
99
+ "change_request_processor", "Starting change request processing",
100
+ pr_number: number, pr_title: pr[:title]
101
+ )
102
+
72
103
  unless @config[:enabled]
73
- 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
+ )
74
108
  return
75
109
  end
76
110
 
77
111
  # Check clarification round limit
78
112
  existing_data = @state_store.change_request_data(number)
79
113
  if existing_data && existing_data["clarification_count"].to_i >= MAX_CLARIFICATION_ROUNDS
80
- 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
+ )
81
118
  post_max_rounds_comment(pr)
82
119
  return
83
120
  end
84
121
 
85
- 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
+ )
86
126
 
87
127
  # Fetch PR details
88
128
  pr_data = @repository_client.fetch_pull_request(number)
@@ -92,29 +132,135 @@ module Aidp
92
132
  authorized_comments = filter_authorized_comments(comments, pr_data)
93
133
 
94
134
  if authorized_comments.empty?
95
- 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
+ )
96
139
  return
97
140
  end
98
141
 
99
- # If max_diff_size is set, attempt to fetch and check diff
100
- # But bypass restriction for worktree-based workflows
142
+ # Fetch diff to check size with enhanced strategy
101
143
  diff = @repository_client.fetch_pull_request_diff(number)
102
144
  diff_size = diff.lines.count
103
145
 
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?
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
+ )
106
158
 
107
- if diff_size > @config[:max_diff_size] && !use_worktree_bypass
108
- display_message("⚠️ PR ##{number} diff too large (#{diff_size} lines > #{@config[:max_diff_size]}). Skipping.", type: :warn)
109
- post_diff_too_large_comment(pr, diff_size)
110
- return
111
- end
159
+ display_message(
160
+ "⚠️ Large PR detected - applying enhanced worktree handling strategy.",
161
+ type: :info
162
+ )
112
163
 
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)
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
115
257
 
116
258
  # Analyze change requests
117
- 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
+ )
118
264
 
119
265
  if analysis_result[:needs_clarification]
120
266
  handle_clarification_needed(pr: pr_data, analysis: analysis_result)
@@ -124,8 +270,17 @@ module Aidp
124
270
  handle_cannot_implement(pr: pr_data, analysis: analysis_result)
125
271
  end
126
272
  rescue => e
127
- display_message("❌ Change request processing failed: #{e.message}", type: :error)
128
- 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
+ )
129
284
 
130
285
  # Record failure state internally but DON'T post error to GitHub
131
286
  # (per issue #280 - error messages should never appear on issues)
@@ -172,19 +327,41 @@ module Aidp
172
327
  # Parse JSON response
173
328
  parsed = JSON.parse(json_content)
174
329
 
175
- {
330
+ # Additional structured analysis
331
+ result = {
176
332
  can_implement: parsed["can_implement"],
177
333
  needs_clarification: parsed["needs_clarification"],
178
334
  clarifying_questions: parsed["clarifying_questions"] || [],
179
335
  reason: parsed["reason"],
180
- changes: parsed["changes"] || []
336
+ changes: []
181
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
182
359
  rescue JSON::ParserError => e
183
360
  Aidp.log_error("change_request_processor", "Failed to parse AI response", error: e.message, content: content)
184
- {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: []}
185
362
  rescue => e
186
363
  Aidp.log_error("change_request_processor", "AI analysis failed", error: e.message)
187
- {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: []}
188
365
  end
189
366
 
190
367
  def change_request_system_prompt
@@ -313,129 +490,351 @@ module Aidp
313
490
  end
314
491
 
315
492
  def checkout_pr_branch(pr_data)
316
- head_ref = pr_data[:head_ref]
317
493
  pr_number = pr_data[:number]
494
+ base_branch = pr_data[:base_ref]
318
495
 
319
- worktree_path = resolve_worktree_for_pr(pr_data)
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
+ )
320
502
 
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
503
+ # Get PR branch information
504
+ head_ref = @worktree_branch_manager.get_pr_branch(pr_number)
326
505
 
327
- @project_dir = worktree_path
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
+ )
328
512
 
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
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
+ }
332
520
 
333
- def resolve_worktree_for_pr(pr_data)
334
- head_ref = pr_data[:head_ref]
335
- pr_number = pr_data[:number]
521
+ # Enhanced logging for worktree preparation
522
+ Aidp.log_info(
523
+ "change_request_processor", "Preparing PR worktree",
524
+ **log_worktree_strategy
525
+ )
336
526
 
337
- existing = Aidp::Worktree.find_by_branch(branch: head_ref, project_dir: @project_dir)
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
338
541
 
339
- if existing && existing[:active]
340
- display_message("🔄 Using existing worktree for branch: #{head_ref}", type: :info)
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
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
+ )
344
548
 
345
- issue_worktree = find_issue_worktree_for_pr(pr_data)
346
- return issue_worktree if issue_worktree
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
+ )
556
+
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
585
+
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
+ }
594
+
595
+ Aidp.log_debug(
596
+ "change_request_processor", "Validated PR worktree",
597
+ **worktree_details
598
+ )
347
599
 
348
- create_worktree_for_pr(pr_data)
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
+ )
611
+
612
+ # Provide detailed error handling and recovery
613
+ handle_worktree_error(pr_data, e)
614
+ end
349
615
  end
350
616
 
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)
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
+ )
625
+
626
+ # Post a comment to GitHub about the failure
627
+ comment_body = <<~COMMENT
628
+ #{COMMENT_HEADER}
354
629
 
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
630
+ Automated worktree preparation failed for this pull request.
358
631
 
359
- linked_issue_numbers.each do |issue_number|
360
- workstream = @state_store.workstream_for_issue(issue_number)
361
- next unless workstream
632
+ **Error Details:**
633
+ ```
634
+ #{error.message}
635
+ ```
362
636
 
363
- slug = workstream[:workstream]
364
- branch = workstream[:branch]
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
365
641
 
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
373
- end
642
+ This may require manual intervention or administrative access.
643
+ COMMENT
374
644
 
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
645
+ begin
646
+ @repository_client.post_comment(pr_data[:number], comment_body)
647
+
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
+ )
383
660
  end
384
661
 
385
- nil
662
+ # Re-raise the original error to halt processing
663
+ raise error
386
664
  end
387
665
 
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
666
+ private
391
667
 
392
- issue_matches.map { |num| num.to_i }.uniq
393
- end
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
+ }
394
686
 
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"
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"])
399
690
 
400
- display_message("🌿 Creating worktree for PR ##{pr_number}: #{head_ref}", type: :info)
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)
401
698
 
402
- Dir.chdir(@project_dir) do
403
- run_git(%w[fetch origin], allow_failure: true)
404
- end
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
+ }
405
777
 
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
- )
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
+ }
412
808
 
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)
809
+ results[:errors] << error_details
416
810
 
417
- worktree_path
418
- end
811
+ Aidp.log_error("change_request_processor", "Change application failed",
812
+ **error_details)
419
813
 
420
- def apply_changes(changes)
421
- changes.each do |change|
422
- file_path = File.join(@project_dir, change["file"])
423
-
424
- case change["action"]
425
- when "create", "edit"
426
- FileUtils.mkdir_p(File.dirname(file_path))
427
- File.write(file_path, change["content"])
428
- display_message(" ✓ #{change["action"]} #{change["file"]}", type: :muted) if @verbose
429
- Aidp.log_debug("change_request_processor", "Applied change", action: change["action"], file: change["file"])
430
- when "delete"
431
- File.delete(file_path) if File.exist?(file_path)
432
- display_message(" ✓ Deleted #{change["file"]}", type: :muted) if @verbose
433
- Aidp.log_debug("change_request_processor", "Deleted file", file: change["file"])
434
- else
435
- display_message(" ⚠️ Unknown action: #{change["action"]} for #{change["file"]}", type: :warn)
436
- 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)
437
815
  end
438
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
439
838
  end
440
839
 
441
840
  def run_tests_and_linters
@@ -466,30 +865,96 @@ module Aidp
466
865
 
467
866
  def commit_and_push(pr_data, analysis)
468
867
  Dir.chdir(@project_dir) do
469
- # Check if there are changes
470
- status_output = run_git(%w[status --porcelain])
471
- if status_output.strip.empty?
472
- 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
+ )
473
875
  return false
474
876
  end
475
877
 
476
- # Stage all changes
477
- run_git(%w[add -A])
478
-
479
- # Create commit
480
- commit_message = build_commit_message(pr_data, analysis)
481
- run_git(["commit", "-m", commit_message])
482
-
483
- display_message("💾 Created commit: #{commit_message.lines.first.strip}", type: :info)
484
- Aidp.log_debug("change_request_processor", "Created commit", pr: pr_data[:number])
485
-
486
- # Push to origin
487
- head_ref = pr_data[:head_ref]
488
- run_git(["push", "origin", head_ref])
489
-
490
- display_message("⬆️ Pushed changes to #{head_ref}", type: :success)
491
- Aidp.log_info("change_request_processor", "Pushed changes", pr: pr_data[:number], branch: head_ref)
492
- 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
493
958
  end
494
959
  end
495
960
 
@@ -774,36 +1239,56 @@ module Aidp
774
1239
  end
775
1240
 
776
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
+
777
1252
  comment = <<~COMMENT
778
1253
  #{COMMENT_HEADER}
779
1254
 
780
- ⚠️ PR diff is too large for default change requests.
1255
+ ⚠️ PR diff is too large for standard automated change requests.
781
1256
 
782
1257
  **Current size:** #{diff_size} lines
783
1258
  **Maximum allowed:** #{@config[:max_diff_size]} lines
1259
+ **Handling strategy:** #{handling_strategy}
784
1260
 
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.
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
794
1266
  COMMENT
795
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
+
796
1276
  begin
797
1277
  @repository_client.post_comment(pr[:number], comment)
798
1278
  @repository_client.remove_labels(pr[:number], @change_request_label)
799
- rescue
800
- 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
+ )
801
1285
  end
802
1286
  end
803
1287
 
804
1288
  def run_git(args, allow_failure: false)
805
- stdout, stderr, status = Open3.capture3("git", *Array(args))
806
- 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
807
1292
  stdout
808
1293
  end
809
1294
 
@@ -839,6 +1324,53 @@ module Aidp
839
1324
  end
840
1325
  end
841
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
+
842
1374
  def symbolize_keys(hash)
843
1375
  return {} unless hash
844
1376