aidp 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcf4f30f93195b349c527dd5531e056ad566a34be1ed079ced4b5002f07a93f5
4
- data.tar.gz: 22a7ef871ab691e0072a1bc8f1254784e1605f46d3edfc9585e91c3a21c5180f
3
+ metadata.gz: de5176199a74d1e4992451708e3830fff889cffd526c8bc0887a0aa73a4946a9
4
+ data.tar.gz: 399593a4d2b8d6991d37d22f383c3cd816b5b276b42e1aa45b0a2df064a470da
5
5
  SHA512:
6
- metadata.gz: c5630eb8255464816006bb091b02666b29ac58b4a1e3e9136c17f79dff54762d6ebe16a4ec45df3f26c22047959121db04ef4e94904be5a4fb2e03320c6a6113
7
- data.tar.gz: 5bdae7edfb7aad8ef03dd2274c4a8095bd927097cbd882a28856f0f753b46c4f1f0d8f756801c6df239bd13540355cbf9e53805b3bc815e5ac1cfc407d61c1c9
6
+ metadata.gz: 1d290891c58232da0b32ca1a8560ba211f5a5805c8aa7b24d78f1536cd4e32750ca9ee11dbb8c1b25f018ed50082c5fd52e9412f7b871981f0ff8eacef6f5e15
7
+ data.tar.gz: c10ee2a196880353991978cdf7a92775e13828b82452ba323c152853740a402a07bad29dad8984da1f9f0e2f650e5075bde9308ac8cc326b89de72af66652b2a
data/lib/aidp/config.rb CHANGED
@@ -111,11 +111,10 @@ module Aidp
111
111
  model_family: "claude",
112
112
  max_tokens: 100_000,
113
113
  default_flags: ["--dangerously-skip-permissions"],
114
- models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"],
114
+ models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"],
115
115
  model_weights: {
116
116
  "claude-3-5-sonnet-20241022" => 3,
117
- "claude-3-5-haiku-20241022" => 2,
118
- "claude-3-opus-20240229" => 1
117
+ "claude-3-5-haiku-20241022" => 2
119
118
  },
120
119
  models_config: {
121
120
  "claude-3-5-sonnet-20241022" => {
@@ -127,11 +126,6 @@ module Aidp
127
126
  flags: ["--dangerously-skip-permissions"],
128
127
  max_tokens: 200_000,
129
128
  timeout: 180
130
- },
131
- "claude-3-opus-20240229" => {
132
- flags: ["--dangerously-skip-permissions"],
133
- max_tokens: 200_000,
134
- timeout: 600
135
129
  }
136
130
  },
137
131
  auth: {
@@ -1057,7 +1057,7 @@ module Aidp
1057
1057
  def default_thinking_config
1058
1058
  {
1059
1059
  default_tier: "mini", # Use mini tier by default for cost optimization
1060
- max_tier: "max",
1060
+ max_tier: "pro", # Max tier rarely needed; pro is sufficient for most tasks
1061
1061
  allow_provider_switch: true,
1062
1062
  auto_escalate: true,
1063
1063
  escalation_threshold: 2,
@@ -1394,9 +1394,9 @@ module Aidp
1394
1394
 
1395
1395
  # Execute a prompt with a specific provider
1396
1396
  def execute_with_provider(provider_type, prompt, options = {})
1397
- # Extract model and tier from options if provided
1397
+ # Extract model from options if provided
1398
1398
  model_name = options.delete(:model)
1399
- tier = options[:tier] # Keep tier in options for provider
1399
+ retry_on_rate_limit = options.delete(:retry_on_rate_limit) != false # Default true
1400
1400
 
1401
1401
  # Create provider factory instance
1402
1402
  provider_factory = ProviderFactory.new
@@ -1415,11 +1415,10 @@ module Aidp
1415
1415
  Aidp.logger.debug("provider_manager", "Executing with provider",
1416
1416
  provider: provider_type,
1417
1417
  model: model_name,
1418
- tier: tier,
1419
1418
  prompt_length: prompt.length)
1420
1419
 
1421
- # Execute the prompt with the provider (pass options including tier)
1422
- result = provider.send_message(prompt: prompt, session: nil, options: options)
1420
+ # Execute the prompt with the provider
1421
+ result = provider.send_message(prompt: prompt, session: nil)
1423
1422
 
1424
1423
  # Return structured result
1425
1424
  {
@@ -1436,6 +1435,38 @@ module Aidp
1436
1435
  }
1437
1436
  rescue => e
1438
1437
  log_rescue(e, component: "provider_manager", action: "execute_with_provider", fallback: "error_result", provider: provider_type, model: model_name, prompt_length: prompt.length)
1438
+
1439
+ # Detect rate limit / quota errors and attempt fallback
1440
+ error_message = e.message.to_s.downcase
1441
+ is_rate_limit = error_message.include?("rate limit") ||
1442
+ error_message.include?("quota") ||
1443
+ error_message.include?("limit reached") ||
1444
+ error_message.include?("resource exhausted") ||
1445
+ error_message.include?("too many requests")
1446
+
1447
+ if is_rate_limit && retry_on_rate_limit
1448
+ Aidp.logger.warn("provider_manager", "Rate limit detected, attempting fallback",
1449
+ provider: provider_type,
1450
+ model: model_name,
1451
+ error: e.message)
1452
+
1453
+ # Attempt to switch to fallback provider
1454
+ fallback_provider = switch_provider_for_error("rate_limit", {
1455
+ original_provider: provider_type,
1456
+ model: model_name,
1457
+ error_message: e.message
1458
+ })
1459
+
1460
+ if fallback_provider && fallback_provider != provider_type
1461
+ Aidp.logger.info("provider_manager", "Retrying with fallback provider",
1462
+ original: provider_type,
1463
+ fallback: fallback_provider)
1464
+
1465
+ # Retry with fallback provider (disable retry to prevent infinite loop)
1466
+ return execute_with_provider(fallback_provider, prompt, options.merge(retry_on_rate_limit: false))
1467
+ end
1468
+ end
1469
+
1439
1470
  # Return error result
1440
1471
  {
1441
1472
  status: "error",
@@ -408,20 +408,10 @@ module Aidp
408
408
  Aidp.log_debug("anthropic_provider", "error_classified",
409
409
  exit_code: result.exit_status,
410
410
  type: error_classification[:type],
411
- confidence: error_classification[:confidence]) # Check for rate limit
412
- if error_classification[:is_rate_limit]
413
- Aidp.log_debug("anthropic_provider", "rate_limit_detected",
414
- exit_code: result.exit_status,
415
- confidence: error_classification[:confidence],
416
- message: combined)
417
- notify_rate_limit(combined)
418
- error_message = "Rate limit reached for Claude CLI.\n#{combined}"
419
- error = RuntimeError.new(error_message)
420
- debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
421
- raise error
422
- end
411
+ confidence: error_classification[:confidence])
423
412
 
424
- # Check for model deprecation
413
+ # Check for model deprecation FIRST (before rate limiting)
414
+ # Even if rate limited, we need to cache the deprecation for next run
425
415
  if error_classification[:is_deprecation]
426
416
  deprecated_model = @model
427
417
  Aidp.log_error("anthropic", "Model deprecation detected",
@@ -448,7 +438,7 @@ module Aidp
448
438
  # Update model and retry
449
439
  @model = replacement
450
440
 
451
- # Retry with new model
441
+ # Retry with new model (even if rate limited, we'll hit rate limit with new model)
452
442
  debug_log("🔄 Retrying with upgraded model: #{replacement}", level: :info)
453
443
  return send_message(prompt: prompt, session: session, options: options)
454
444
  else
@@ -469,6 +459,19 @@ module Aidp
469
459
  end
470
460
  end
471
461
 
462
+ # Check for rate limit (after handling deprecation)
463
+ if error_classification[:is_rate_limit]
464
+ Aidp.log_debug("anthropic_provider", "rate_limit_detected",
465
+ exit_code: result.exit_status,
466
+ confidence: error_classification[:confidence],
467
+ message: combined)
468
+ notify_rate_limit(combined)
469
+ error_message = "Rate limit reached for Claude CLI.\n#{combined}"
470
+ error = RuntimeError.new(error_message)
471
+ debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
472
+ raise error
473
+ end
474
+
472
475
  # Check for auth issues
473
476
  if combined.downcase.include?("oauth token has expired") || combined.downcase.include?("authentication_error")
474
477
  error_message = "Authentication error from Claude CLI: token expired or invalid.\n" \
@@ -56,7 +56,13 @@ module Aidp
56
56
  "api.cursor.sh",
57
57
  "cursor.sh",
58
58
  "app.cursor.sh",
59
- "www.cursor.sh"
59
+ "www.cursor.sh",
60
+ # Authentication (Auth0)
61
+ "auth.cursor.sh",
62
+ "auth0.com",
63
+ "*.auth0.com",
64
+ "a0core.net",
65
+ "*.a0core.net"
60
66
  ],
61
67
  ip_ranges: []
62
68
  }
@@ -50,6 +50,7 @@ module Aidp
50
50
  return @saved if skip_wizard?
51
51
 
52
52
  configure_providers
53
+ configure_harness_settings
53
54
  configure_thinking_tiers
54
55
  configure_work_loop
55
56
  configure_branching
@@ -285,6 +286,24 @@ module Aidp
285
286
  show_provider_summary(provider_choice, cleaned_fallbacks) unless provider_choice == "custom"
286
287
  end
287
288
 
289
+ # -------------------------------------------
290
+ # Harness settings (retries, limits, etc.)
291
+ # -------------------------------------------
292
+ def configure_harness_settings
293
+ prompt.say("\n⚙️ Harness Configuration")
294
+ prompt.say(" Advanced settings for provider behavior")
295
+ existing = get([:harness]) || {}
296
+
297
+ return unless prompt.yes?("Configure advanced harness settings?", default: false)
298
+
299
+ max_retries = ask_with_default(
300
+ "Maximum retry attempts for failed LLM calls",
301
+ (existing[:max_retries] || 2).to_s
302
+ ) { |value| value.to_i }
303
+
304
+ set([:harness, :max_retries], max_retries)
305
+ end
306
+
288
307
  # Removed MCP configuration step (MCP now expected to be provider-specific if used)
289
308
 
290
309
  # -------------------------------------------
@@ -473,6 +492,7 @@ module Aidp
473
492
  prompt.say("\n⚙️ Work loop configuration")
474
493
  prompt.say("-" * 40)
475
494
 
495
+ configure_work_loop_limits
476
496
  configure_test_commands
477
497
  configure_linting
478
498
  configure_watch_patterns
@@ -482,6 +502,19 @@ module Aidp
482
502
  configure_vcs_behavior
483
503
  end
484
504
 
505
+ def configure_work_loop_limits
506
+ existing = get([:work_loop]) || {}
507
+
508
+ return unless prompt.yes?("Configure work loop limits?", default: false)
509
+
510
+ max_iterations = ask_with_default(
511
+ "Maximum work loop iterations",
512
+ (existing[:max_iterations] || 50).to_s
513
+ ) { |value| value.to_i }
514
+
515
+ set([:work_loop, :max_iterations], max_iterations)
516
+ end
517
+
485
518
  def configure_test_commands
486
519
  existing = get([:work_loop, :test]) || {}
487
520
 
@@ -999,6 +1032,7 @@ module Aidp
999
1032
 
1000
1033
  configure_watch_safety
1001
1034
  configure_watch_labels
1035
+ configure_watch_change_requests
1002
1036
  configure_watch_label_creation
1003
1037
  end
1004
1038
 
@@ -1066,6 +1100,11 @@ module Aidp
1066
1100
  existing[:ci_fix_trigger] || "aidp-fix-ci"
1067
1101
  )
1068
1102
 
1103
+ auto_trigger = ask_with_default(
1104
+ "Label to trigger fully autonomous build+review+CI",
1105
+ existing[:auto_trigger] || "aidp-auto"
1106
+ )
1107
+
1069
1108
  change_request_trigger = ask_with_default(
1070
1109
  "Label to trigger PR change implementation",
1071
1110
  existing[:change_request_trigger] || "aidp-request-changes"
@@ -1078,10 +1117,35 @@ module Aidp
1078
1117
  build_trigger: build_trigger,
1079
1118
  review_trigger: review_trigger,
1080
1119
  ci_fix_trigger: ci_fix_trigger,
1120
+ auto_trigger: auto_trigger,
1081
1121
  change_request_trigger: change_request_trigger
1082
1122
  })
1083
1123
  end
1084
1124
 
1125
+ def configure_watch_change_requests
1126
+ prompt.say("\n📝 PR Change Request Configuration")
1127
+ prompt.say(" Configure how AIDP handles automated PR change requests")
1128
+ existing = get([:watch, :change_requests]) || {}
1129
+
1130
+ max_diff_size = ask_with_default(
1131
+ "Maximum PR diff size (lines) for change requests",
1132
+ (existing[:max_diff_size] || 5000).to_s
1133
+ ) { |value| value.to_i }
1134
+
1135
+ post_comments = prompt.yes?(
1136
+ "Post detection comments when work is detected?",
1137
+ default: existing.fetch(:post_detection_comments, true)
1138
+ )
1139
+
1140
+ set([:watch, :change_requests], {
1141
+ max_diff_size: max_diff_size
1142
+ })
1143
+
1144
+ set([:watch], {
1145
+ post_detection_comments: post_comments
1146
+ }.merge(get([:watch]) || {}))
1147
+ end
1148
+
1085
1149
  def configure_watch_label_creation
1086
1150
  prompt.say("\n🏷️ GitHub Label Auto-Creation")
1087
1151
  prompt.say(" Automatically create GitHub labels for watch mode if they don't exist")
@@ -1222,6 +1286,7 @@ module Aidp
1222
1286
  build_trigger: "5319E7", # Purple
1223
1287
  review_trigger: "FBCA04", # Yellow
1224
1288
  ci_fix_trigger: "D93F0B", # Red
1289
+ auto_trigger: "0C8BD6", # Blue (distinct from build)
1225
1290
  change_request_trigger: "F9D0C4", # Light pink
1226
1291
  in_progress: "1D76DB" # Dark blue (internal coordination)
1227
1292
  }
data/lib/aidp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.30.0"
4
+ VERSION = "0.31.0"
5
5
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../message_display"
4
+ require_relative "github_state_extractor"
5
+
6
+ module Aidp
7
+ module Watch
8
+ # Handles the aidp-auto label on PRs by chaining review and CI-fix flows
9
+ # until the PR is ready for human review.
10
+ class AutoPrProcessor
11
+ include Aidp::MessageDisplay
12
+
13
+ DEFAULT_AUTO_LABEL = "aidp-auto"
14
+
15
+ def initialize(repository_client:, state_store:, review_processor:, ci_fix_processor:, label_config: {}, verbose: false)
16
+ @repository_client = repository_client
17
+ @state_store = state_store
18
+ @review_processor = review_processor
19
+ @ci_fix_processor = ci_fix_processor
20
+ @state_extractor = GitHubStateExtractor.new(repository_client: repository_client)
21
+ @verbose = verbose
22
+ @auto_label = label_config[:auto_trigger] || label_config["auto_trigger"] || DEFAULT_AUTO_LABEL
23
+ end
24
+
25
+ def process(pr)
26
+ number = pr[:number]
27
+ Aidp.log_debug("auto_pr_processor", "process_started", pr: number, title: pr[:title])
28
+ display_message("🤖 Running autonomous review/CI loop for PR ##{number}", type: :info)
29
+
30
+ # Run review and CI fix flows. Each processor is responsible for its own guards.
31
+ @review_processor.process(pr)
32
+ @ci_fix_processor.process(pr)
33
+
34
+ finalize_if_ready(pr_number: number)
35
+ rescue => e
36
+ Aidp.log_error("auto_pr_processor", "process_failed", pr: pr[:number], error: e.message, error_class: e.class.name)
37
+ display_message("❌ aidp-auto failed on PR ##{pr[:number]}: #{e.message}", type: :error)
38
+ end
39
+
40
+ attr_reader :auto_label
41
+
42
+ private
43
+
44
+ def finalize_if_ready(pr_number:)
45
+ pr_data = @repository_client.fetch_pull_request(pr_number)
46
+ ci_status = @repository_client.fetch_ci_status(pr_number)
47
+
48
+ review_done = @state_extractor.review_completed?(pr_data) || @state_store.review_processed?(pr_number)
49
+ ci_passing = ci_status[:state] == "success"
50
+
51
+ Aidp.log_debug("auto_pr_processor", "completion_check",
52
+ pr: pr_number,
53
+ review_done: review_done,
54
+ ci_state: ci_status[:state])
55
+
56
+ return unless review_done && ci_passing
57
+
58
+ post_completion_comment(pr_number)
59
+ remove_auto_label(pr_number)
60
+ end
61
+
62
+ def post_completion_comment(pr_number)
63
+ comment = <<~COMMENT
64
+ ## 🤖 aidp-auto
65
+
66
+ - Automated review completed
67
+ - CI is passing
68
+
69
+ Marking this PR ready for human review and removing the `#{@auto_label}` label.
70
+ COMMENT
71
+
72
+ @repository_client.post_comment(pr_number, comment)
73
+ display_message("💬 Posted aidp-auto completion comment on PR ##{pr_number}", type: :success)
74
+ rescue => e
75
+ Aidp.log_warn("auto_pr_processor", "comment_failed", pr: pr_number, error: e.message)
76
+ end
77
+
78
+ def remove_auto_label(pr_number)
79
+ @repository_client.remove_labels(pr_number, @auto_label)
80
+ display_message("🏷️ Removed '#{@auto_label}' from PR ##{pr_number}", type: :info)
81
+ rescue => e
82
+ Aidp.log_warn("auto_pr_processor", "remove_label_failed", pr: pr_number, error: e.message)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../message_display"
4
+
5
+ module Aidp
6
+ module Watch
7
+ # Handles the aidp-auto label on issues by delegating to the BuildProcessor
8
+ # and transferring the label to the created PR once work completes.
9
+ class AutoProcessor
10
+ include Aidp::MessageDisplay
11
+
12
+ DEFAULT_AUTO_LABEL = "aidp-auto"
13
+
14
+ attr_reader :auto_label
15
+
16
+ def initialize(repository_client:, state_store:, build_processor:, label_config: {}, verbose: false)
17
+ @repository_client = repository_client
18
+ @state_store = state_store
19
+ @build_processor = build_processor
20
+ @verbose = verbose
21
+
22
+ # Allow overrides from watch config
23
+ @auto_label = label_config[:auto_trigger] || label_config["auto_trigger"] || DEFAULT_AUTO_LABEL
24
+ end
25
+
26
+ def process(issue)
27
+ number = issue[:number]
28
+ Aidp.log_debug("auto_processor", "process_started", issue: number, title: issue[:title])
29
+ display_message("🤖 Starting autonomous build for issue ##{number}", type: :info)
30
+
31
+ @build_processor.process(issue)
32
+
33
+ status = @state_store.build_status(number)
34
+ pr_url = status["pr_url"]
35
+ pr_number = extract_pr_number(pr_url)
36
+
37
+ unless status["status"] == "completed" && pr_number
38
+ Aidp.log_debug("auto_processor", "no_pr_to_transfer", issue: number, status: status["status"], pr_url: pr_url)
39
+ return
40
+ end
41
+
42
+ transfer_label_to_pr(issue_number: number, pr_number: pr_number)
43
+ rescue => e
44
+ Aidp.log_error("auto_processor", "process_failed", issue: issue[:number], error: e.message, error_class: e.class.name)
45
+ display_message("❌ aidp-auto failed for issue ##{issue[:number]}: #{e.message}", type: :error)
46
+ end
47
+
48
+ private
49
+
50
+ def extract_pr_number(pr_url)
51
+ return nil unless pr_url
52
+
53
+ match = pr_url.match(%r{/pull/(\d+)}i)
54
+ match && match[1].to_i
55
+ end
56
+
57
+ def transfer_label_to_pr(issue_number:, pr_number:)
58
+ Aidp.log_debug("auto_processor", "transferring_label", issue: issue_number, pr: pr_number, label: @auto_label)
59
+
60
+ begin
61
+ @repository_client.add_labels(pr_number, @auto_label)
62
+ display_message("🏷️ Added '#{@auto_label}' to PR ##{pr_number}", type: :info)
63
+ rescue => e
64
+ Aidp.log_warn("auto_processor", "add_label_failed", pr: pr_number, label: @auto_label, error: e.message)
65
+ display_message("⚠️ Failed to add '#{@auto_label}' to PR ##{pr_number}: #{e.message}", type: :warn)
66
+ end
67
+
68
+ begin
69
+ @repository_client.remove_labels(issue_number, @auto_label)
70
+ display_message("🏷️ Removed '#{@auto_label}' from issue ##{issue_number}", type: :muted)
71
+ rescue => e
72
+ Aidp.log_warn("auto_processor", "remove_label_failed", issue: issue_number, label: @auto_label, error: e.message)
73
+ display_message("⚠️ Failed to remove '#{@auto_label}' from issue ##{issue_number}: #{e.message}", type: :warn)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -57,7 +57,8 @@ module Aidp
57
57
  run_tests_before_push: true,
58
58
  commit_message_prefix: "aidp: pr-change",
59
59
  require_comment_reference: true,
60
- max_diff_size: 2000
60
+ max_diff_size: 2000,
61
+ allow_large_pr_worktree_bypass: true # Default to always using worktree for large PRs
61
62
  }.merge(symbolize_keys(change_request_config))
62
63
 
63
64
  # Load safety configuration
@@ -95,16 +96,23 @@ module Aidp
95
96
  return
96
97
  end
97
98
 
98
- # Fetch diff to check size
99
+ # If max_diff_size is set, attempt to fetch and check diff
100
+ # But bypass restriction for worktree-based workflows
99
101
  diff = @repository_client.fetch_pull_request_diff(number)
100
102
  diff_size = diff.lines.count
101
103
 
102
- if diff_size > @config[:max_diff_size]
104
+ # Check if we want to use the worktree bypass
105
+ use_worktree_bypass = @config[:allow_large_pr_worktree_bypass] || @config[:allow_large_pr_worktree_bypass].nil?
106
+
107
+ if diff_size > @config[:max_diff_size] && !use_worktree_bypass
103
108
  display_message("⚠️ PR ##{number} diff too large (#{diff_size} lines > #{@config[:max_diff_size]}). Skipping.", type: :warn)
104
109
  post_diff_too_large_comment(pr, diff_size)
105
110
  return
106
111
  end
107
112
 
113
+ # Log the diff size for observability
114
+ Aidp.log_debug("change_request_processor", "PR diff size", number: number, size: diff_size, max_allowed: @config[:max_diff_size], worktree_bypass: use_worktree_bypass)
115
+
108
116
  # Analyze change requests
109
117
  analysis_result = analyze_change_requests(pr_data: pr_data, comments: authorized_comments, diff: diff)
110
118
 
@@ -308,40 +316,105 @@ module Aidp
308
316
  head_ref = pr_data[:head_ref]
309
317
  pr_number = pr_data[:number]
310
318
 
311
- # Check if a worktree already exists for this branch
319
+ worktree_path = resolve_worktree_for_pr(pr_data)
320
+
321
+ Dir.chdir(worktree_path) do
322
+ run_git(%w[fetch origin], allow_failure: true)
323
+ run_git(["checkout", head_ref])
324
+ run_git(%w[pull --ff-only], allow_failure: true)
325
+ end
326
+
327
+ @project_dir = worktree_path
328
+
329
+ Aidp.log_debug("change_request_processor", "Checked out PR branch", branch: head_ref, worktree: worktree_path)
330
+ display_message("🌿 Using worktree for PR ##{pr_number}: #{head_ref}", type: :info)
331
+ end
332
+
333
+ def resolve_worktree_for_pr(pr_data)
334
+ head_ref = pr_data[:head_ref]
335
+ pr_number = pr_data[:number]
336
+
312
337
  existing = Aidp::Worktree.find_by_branch(branch: head_ref, project_dir: @project_dir)
313
338
 
314
339
  if existing && existing[:active]
315
340
  display_message("🔄 Using existing worktree for branch: #{head_ref}", type: :info)
316
341
  Aidp.log_debug("change_request_processor", "worktree_reused", pr_number: pr_number, branch: head_ref, path: existing[:path])
342
+ return existing[:path]
343
+ end
344
+
345
+ issue_worktree = find_issue_worktree_for_pr(pr_data)
346
+ return issue_worktree if issue_worktree
347
+
348
+ create_worktree_for_pr(pr_data)
349
+ end
350
+
351
+ def find_issue_worktree_for_pr(pr_data)
352
+ pr_number = pr_data[:number]
353
+ linked_issue_numbers = extract_issue_numbers_from_pr(pr_data)
354
+
355
+ build_match = @state_store.find_build_by_pr(pr_number)
356
+ linked_issue_numbers << build_match[:issue_number] if build_match
357
+ linked_issue_numbers = linked_issue_numbers.compact.uniq
317
358
 
318
- # Update @project_dir to point to the worktree
319
- @project_dir = existing[:path]
359
+ linked_issue_numbers.each do |issue_number|
360
+ workstream = @state_store.workstream_for_issue(issue_number)
361
+ next unless workstream
320
362
 
321
- # Pull latest changes in the worktree
322
- Dir.chdir(@project_dir) do
323
- run_git(%w[fetch origin], allow_failure: true)
324
- run_git(["checkout", head_ref])
325
- run_git(%w[pull --ff-only], allow_failure: true)
363
+ slug = workstream[:workstream]
364
+ branch = workstream[:branch]
365
+
366
+ if slug
367
+ info = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
368
+ if info && info[:active]
369
+ Aidp.log_debug("change_request_processor", "issue_worktree_reused", pr_number: pr_number, issue_number: issue_number, branch: branch, path: info[:path])
370
+ display_message("🔄 Reusing worktree #{slug} for issue ##{issue_number} (PR ##{pr_number})", type: :info)
371
+ return info[:path]
372
+ end
326
373
  end
327
374
 
328
- return
375
+ if branch
376
+ existing = Aidp::Worktree.find_by_branch(branch: branch, project_dir: @project_dir)
377
+ if existing && existing[:active]
378
+ Aidp.log_debug("change_request_processor", "issue_branch_worktree_reused", pr_number: pr_number, issue_number: issue_number, branch: branch, path: existing[:path])
379
+ display_message("🔄 Reusing branch worktree for issue ##{issue_number}: #{branch}", type: :info)
380
+ return existing[:path]
381
+ end
382
+ end
329
383
  end
330
384
 
331
- # Otherwise, use the main worktree
332
- Dir.chdir(@project_dir) do
333
- # Fetch latest
334
- run_git(%w[fetch origin])
385
+ nil
386
+ end
335
387
 
336
- # Checkout the PR branch
337
- run_git(["checkout", head_ref])
388
+ def extract_issue_numbers_from_pr(pr_data)
389
+ body = pr_data[:body].to_s
390
+ issue_matches = body.scan(/(?:Fixes|Resolves|Closes)\s+#(\d+)/i).flatten
338
391
 
339
- # Pull latest changes
340
- run_git(%w[pull --ff-only], allow_failure: true)
392
+ issue_matches.map { |num| num.to_i }.uniq
393
+ end
394
+
395
+ def create_worktree_for_pr(pr_data)
396
+ head_ref = pr_data[:head_ref]
397
+ pr_number = pr_data[:number]
398
+ slug = "pr-#{pr_number}-change-requests"
399
+
400
+ display_message("🌿 Creating worktree for PR ##{pr_number}: #{head_ref}", type: :info)
401
+
402
+ Dir.chdir(@project_dir) do
403
+ run_git(%w[fetch origin], allow_failure: true)
341
404
  end
342
405
 
343
- Aidp.log_debug("change_request_processor", "Checked out PR branch", branch: head_ref)
344
- display_message("🌿 Checked out branch: #{head_ref}", type: :info)
406
+ result = Aidp::Worktree.create(
407
+ slug: slug,
408
+ project_dir: @project_dir,
409
+ branch: head_ref,
410
+ base_branch: pr_data[:base_ref]
411
+ )
412
+
413
+ worktree_path = result[:path]
414
+ Aidp.log_debug("change_request_processor", "worktree_created", pr_number: pr_number, branch: head_ref, path: worktree_path)
415
+ display_message("✅ Worktree created at #{worktree_path}", type: :success)
416
+
417
+ worktree_path
345
418
  end
346
419
 
347
420
  def apply_changes(changes)
@@ -704,15 +777,20 @@ module Aidp
704
777
  comment = <<~COMMENT
705
778
  #{COMMENT_HEADER}
706
779
 
707
- ⚠️ PR diff is too large for automated change requests.
780
+ ⚠️ PR diff is too large for default change requests.
708
781
 
709
782
  **Current size:** #{diff_size} lines
710
783
  **Maximum allowed:** #{@config[:max_diff_size]} lines
711
784
 
712
- For large PRs, please consider:
713
- 1. Breaking the PR into smaller chunks
714
- 2. Implementing changes manually
715
- 3. Increasing `max_diff_size` in your `aidp.yml` configuration if appropriate
785
+ For large PRs, you have several options:
786
+ 1. Enable worktree-based large PR handling:
787
+ Set `allow_large_pr_worktree_bypass: true` in your `aidp.yml`
788
+ 2. Break the PR into smaller chunks
789
+ 3. Implement changes manually
790
+ 4. Increase `max_diff_size` in your configuration
791
+
792
+ The worktree bypass allows processing large PRs by working directly in the branch
793
+ instead of using diff-based changes.
716
794
  COMMENT
717
795
 
718
796
  begin
@@ -14,6 +14,8 @@ require_relative "../auto_update"
14
14
  require_relative "review_processor"
15
15
  require_relative "ci_fix_processor"
16
16
  require_relative "change_request_processor"
17
+ require_relative "auto_processor"
18
+ require_relative "auto_pr_processor"
17
19
 
18
20
  module Aidp
19
21
  module Watch
@@ -70,6 +72,13 @@ module Aidp
70
72
  verbose: verbose,
71
73
  label_config: label_config
72
74
  )
75
+ @auto_processor = AutoProcessor.new(
76
+ repository_client: @repository_client,
77
+ state_store: @state_store,
78
+ build_processor: @build_processor,
79
+ label_config: label_config,
80
+ verbose: verbose
81
+ )
73
82
 
74
83
  # Initialize auto-update coordinator
75
84
  @auto_update_coordinator = Aidp::AutoUpdate.coordinator(project_dir: project_dir)
@@ -90,6 +99,14 @@ module Aidp
90
99
  label_config: label_config,
91
100
  verbose: verbose
92
101
  )
102
+ @auto_pr_processor = AutoPrProcessor.new(
103
+ repository_client: @repository_client,
104
+ state_store: @state_store,
105
+ review_processor: @review_processor,
106
+ ci_fix_processor: @ci_fix_processor,
107
+ label_config: label_config,
108
+ verbose: verbose
109
+ )
93
110
  @change_request_processor = ChangeRequestProcessor.new(
94
111
  repository_client: @repository_client,
95
112
  state_store: @state_store,
@@ -142,9 +159,11 @@ module Aidp
142
159
  def process_cycle
143
160
  process_plan_triggers
144
161
  process_build_triggers
162
+ process_auto_issue_triggers
145
163
  check_for_updates_if_due
146
164
  process_review_triggers
147
165
  process_ci_fix_triggers
166
+ process_auto_pr_triggers
148
167
  process_change_request_triggers
149
168
  end
150
169
 
@@ -264,6 +283,54 @@ module Aidp
264
283
  end
265
284
  end
266
285
 
286
+ def process_auto_issue_triggers
287
+ auto_label = @auto_processor.auto_label
288
+ begin
289
+ issues = @repository_client.list_issues(labels: [auto_label], state: "open")
290
+ rescue => e
291
+ Aidp.log_error("watch_runner", "auto_issue_poll_failed", label: auto_label, error: e.message)
292
+ return
293
+ end
294
+
295
+ Aidp.log_debug("watch_runner", "auto_issue_poll", label: auto_label, total: issues.size)
296
+
297
+ issues.each do |issue|
298
+ unless issue_has_label?(issue, auto_label)
299
+ Aidp.log_debug("watch_runner", "auto_issue_skip_label_mismatch", issue: issue[:number], labels: issue[:labels])
300
+ next
301
+ end
302
+
303
+ begin
304
+ detailed = @repository_client.fetch_issue(issue[:number])
305
+ rescue => e
306
+ Aidp.log_error("watch_runner", "auto_issue_fetch_failed", issue: issue[:number], error: e.message)
307
+ next
308
+ end
309
+
310
+ # Check if already in progress by another instance
311
+ if @state_extractor.in_progress?(detailed)
312
+ Aidp.log_debug("watch_runner", "auto_issue_skip_in_progress", issue: detailed[:number])
313
+ next
314
+ end
315
+
316
+ # Check author authorization before processing
317
+ unless @safety_checker.should_process_issue?(detailed, enforce: false)
318
+ Aidp.log_debug("watch_runner", "auto_issue_skip_unauthorized_author", issue: detailed[:number], author: detailed[:author])
319
+ next
320
+ end
321
+
322
+ # Check if detection comment already posted (deduplication)
323
+ unless @state_extractor.detection_comment_posted?(detailed, auto_label)
324
+ post_detection_comment(item_type: :issue, number: detailed[:number], label: auto_label)
325
+ end
326
+
327
+ Aidp.log_debug("watch_runner", "auto_issue_process", issue: detailed[:number])
328
+ @auto_processor.process(detailed)
329
+ rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
330
+ Aidp.log_warn("watch_runner", "unauthorized_issue_author_auto", issue: issue[:number], error: e.message)
331
+ end
332
+ end
333
+
267
334
  def process_review_triggers
268
335
  review_label = @review_processor.review_label
269
336
  begin
@@ -310,6 +377,43 @@ module Aidp
310
377
  end
311
378
  end
312
379
 
380
+ def process_auto_pr_triggers
381
+ auto_label = @auto_pr_processor.auto_label
382
+ prs = @repository_client.list_pull_requests(labels: [auto_label], state: "open")
383
+ Aidp.log_debug("watch_runner", "auto_pr_poll", label: auto_label, total: prs.size)
384
+
385
+ prs.each do |pr|
386
+ unless pr_has_label?(pr, auto_label)
387
+ Aidp.log_debug("watch_runner", "auto_pr_skip_label_mismatch", pr: pr[:number], labels: pr[:labels])
388
+ next
389
+ end
390
+
391
+ detailed = @repository_client.fetch_pull_request(pr[:number])
392
+
393
+ # Check if already in progress by another instance
394
+ if @state_extractor.in_progress?(detailed)
395
+ Aidp.log_debug("watch_runner", "auto_pr_skip_in_progress", pr: detailed[:number])
396
+ next
397
+ end
398
+
399
+ # Check author authorization before processing
400
+ unless @safety_checker.should_process_issue?(detailed, enforce: false)
401
+ Aidp.log_debug("watch_runner", "auto_pr_skip_unauthorized_author", pr: detailed[:number], author: detailed[:author])
402
+ next
403
+ end
404
+
405
+ # Check if detection comment already posted (deduplication)
406
+ unless @state_extractor.detection_comment_posted?(detailed, auto_label)
407
+ post_detection_comment(item_type: :pr, number: detailed[:number], label: auto_label)
408
+ end
409
+
410
+ Aidp.log_debug("watch_runner", "auto_pr_process", pr: detailed[:number])
411
+ @auto_pr_processor.process(detailed)
412
+ rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
413
+ Aidp.log_warn("watch_runner", "unauthorized_pr_author_auto", pr: pr[:number], error: e.message)
414
+ end
415
+ end
416
+
313
417
  def process_ci_fix_triggers
314
418
  ci_fix_label = @ci_fix_processor.ci_fix_label
315
419
  prs = @repository_client.list_pull_requests(labels: [ci_fix_label], state: "open")
@@ -65,6 +65,43 @@ module Aidp
65
65
  save!
66
66
  end
67
67
 
68
+ # Retrieve workstream metadata for a given issue
69
+ # @return [Hash, nil] {issue_number:, branch:, workstream:, pr_url:, status:}
70
+ def workstream_for_issue(issue_number)
71
+ data = build_status(issue_number)
72
+ return nil if data.nil? || data.empty?
73
+
74
+ {
75
+ issue_number: issue_number.to_i,
76
+ branch: data["branch"],
77
+ workstream: data["workstream"],
78
+ pr_url: data["pr_url"],
79
+ status: data["status"]
80
+ }
81
+ end
82
+
83
+ # Find the build/workstream metadata associated with a PR URL
84
+ # This is used to map change-request PRs back to their originating issues/worktrees.
85
+ # @return [Hash, nil] {issue_number:, branch:, workstream:, pr_url:, status:}
86
+ def find_build_by_pr(pr_number)
87
+ builds.each do |issue_number, data|
88
+ pr_url = data["pr_url"]
89
+ next unless pr_url
90
+
91
+ if pr_url.match?(%r{/pull/#{pr_number}\b})
92
+ return {
93
+ issue_number: issue_number.to_i,
94
+ branch: data["branch"],
95
+ workstream: data["workstream"],
96
+ pr_url: pr_url,
97
+ status: data["status"]
98
+ }
99
+ end
100
+ end
101
+
102
+ nil
103
+ end
104
+
68
105
  # Review tracking methods
69
106
  def review_processed?(pr_number)
70
107
  reviews.key?(pr_number.to_s)
@@ -0,0 +1,147 @@
1
+ require "json"
2
+ require "fileutils"
3
+
4
+ module Aidp
5
+ # Manages git worktrees for pull request branches
6
+ class WorktreeBranchManager
7
+ class WorktreeCreationError < StandardError; end
8
+ class WorktreeLookupError < StandardError; end
9
+
10
+ # Initialize with a project directory and optional logger
11
+ def initialize(project_dir:, logger: Aidp.logger)
12
+ @project_dir = project_dir
13
+ @logger = logger
14
+ @worktree_registry_path = File.join(project_dir, ".aidp", "worktrees.json")
15
+ end
16
+
17
+ # Find an existing worktree for a given branch or PR
18
+ def find_worktree(branch:)
19
+ Aidp.log_debug("worktree_branch_manager", "finding_worktree", branch: branch)
20
+
21
+ raise WorktreeLookupError, "Invalid git repository: #{@project_dir}" unless git_repository?
22
+
23
+ # Check registry first
24
+ worktree_info = read_registry.find { |w| w["branch"] == branch }
25
+
26
+ if worktree_info
27
+ worktree_path = worktree_info["path"]
28
+ return worktree_path if File.directory?(worktree_path)
29
+ end
30
+
31
+ # Fallback: Use git worktree list to find the worktree
32
+ worktree_list_output = run_git_command("git worktree list")
33
+ worktree_list_output.split("\n").each do |line|
34
+ path, branch_info = line.split(" ", 2)
35
+ return path if branch_info&.include?(branch)
36
+ end
37
+
38
+ nil
39
+ rescue => e
40
+ Aidp.log_error("worktree_branch_manager", "worktree_lookup_failed",
41
+ error: e.message, branch: branch)
42
+ raise
43
+ end
44
+
45
+ # Create a new worktree for a branch
46
+ def create_worktree(branch:, base_branch: "main")
47
+ Aidp.log_debug("worktree_branch_manager", "creating_worktree",
48
+ branch: branch, base_branch: base_branch)
49
+
50
+ # Validate branch name to prevent path traversal
51
+ validate_branch_name!(branch)
52
+
53
+ # Check if worktree already exists
54
+ existing_worktree = find_worktree(branch: branch)
55
+ return existing_worktree if existing_worktree
56
+
57
+ # Ensure base branch exists
58
+ base_ref = (branch == "main") ? "main" : "refs/heads/#{base_branch}"
59
+ base_exists_cmd = "git show-ref --verify --quiet #{base_ref}"
60
+
61
+ system({"GIT_DIR" => File.join(@project_dir, ".git")}, "cd #{@project_dir} && #{base_exists_cmd}")
62
+
63
+ # If base branch doesn't exist locally, create it
64
+ unless $?.success?
65
+ system({"GIT_DIR" => File.join(@project_dir, ".git")}, "cd #{@project_dir} && git checkout -b #{base_branch}")
66
+ end
67
+
68
+ # Create worktree directory
69
+ worktree_name = branch.tr("/", "_")
70
+ worktree_path = File.join(@project_dir, ".worktrees", worktree_name)
71
+
72
+ # Ensure .worktrees directory exists
73
+ FileUtils.mkdir_p(File.join(@project_dir, ".worktrees"))
74
+
75
+ # Create the worktree
76
+ cmd = "git worktree add -b #{branch} #{worktree_path} #{base_branch}"
77
+ result = system({"GIT_DIR" => File.join(@project_dir, ".git")}, "cd #{@project_dir} && #{cmd}")
78
+
79
+ unless result
80
+ Aidp.log_error("worktree_branch_manager", "worktree_creation_failed",
81
+ branch: branch, base_branch: base_branch)
82
+ raise WorktreeCreationError, "Failed to create worktree for branch #{branch}"
83
+ end
84
+
85
+ # Update registry
86
+ update_registry(branch, worktree_path)
87
+
88
+ worktree_path
89
+ end
90
+
91
+ private
92
+
93
+ def git_repository?
94
+ File.directory?(File.join(@project_dir, ".git"))
95
+ rescue
96
+ false
97
+ end
98
+
99
+ def validate_branch_name!(branch)
100
+ if branch.include?("..") || branch.start_with?("/")
101
+ raise WorktreeCreationError, "Invalid branch name: #{branch}"
102
+ end
103
+ end
104
+
105
+ def run_git_command(cmd)
106
+ Dir.chdir(@project_dir) do
107
+ output = `#{cmd} 2>&1`
108
+ raise StandardError, output unless $?.success?
109
+ output
110
+ end
111
+ end
112
+
113
+ # Read the worktree registry
114
+ def read_registry
115
+ return [] unless File.exist?(@worktree_registry_path)
116
+
117
+ begin
118
+ JSON.parse(File.read(@worktree_registry_path))
119
+ rescue JSON::ParserError
120
+ Aidp.log_warn("worktree_branch_manager", "invalid_registry",
121
+ path: @worktree_registry_path)
122
+ []
123
+ end
124
+ end
125
+
126
+ # Update the worktree registry
127
+ def update_registry(branch, path)
128
+ # Ensure .aidp directory exists
129
+ FileUtils.mkdir_p(File.dirname(@worktree_registry_path))
130
+
131
+ registry = read_registry
132
+
133
+ # Remove existing entries for the same branch
134
+ registry.reject! { |w| w["branch"] == branch }
135
+
136
+ # Add new entry
137
+ registry << {
138
+ "branch" => branch,
139
+ "path" => path,
140
+ "created_at" => Time.now.to_i
141
+ }
142
+
143
+ # Write updated registry
144
+ File.write(@worktree_registry_path, JSON.pretty_generate(registry))
145
+ end
146
+ end
147
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aidp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.0
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan
@@ -449,6 +449,8 @@ files:
449
449
  - lib/aidp/utils/devcontainer_detector.rb
450
450
  - lib/aidp/version.rb
451
451
  - lib/aidp/watch.rb
452
+ - lib/aidp/watch/auto_pr_processor.rb
453
+ - lib/aidp/watch/auto_processor.rb
452
454
  - lib/aidp/watch/build_processor.rb
453
455
  - lib/aidp/watch/change_request_processor.rb
454
456
  - lib/aidp/watch/ci_fix_processor.rb
@@ -473,6 +475,7 @@ files:
473
475
  - lib/aidp/workstream_executor.rb
474
476
  - lib/aidp/workstream_state.rb
475
477
  - lib/aidp/worktree.rb
478
+ - lib/aidp/worktree_branch_manager.rb
476
479
  - templates/COMMON/AGENT_BASE.md
477
480
  - templates/COMMON/CONVENTIONS.md
478
481
  - templates/COMMON/TEMPLATES/ADR_TEMPLATE.md