aidp 0.30.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/util.rb CHANGED
@@ -28,6 +28,17 @@ module Aidp
28
28
  File.write(path, content)
29
29
  end
30
30
 
31
+ # Walk upward to find the nearest project root (git/package manager markers)
32
+ def self.find_project_root(start_dir = Dir.pwd)
33
+ dir = File.expand_path(start_dir)
34
+ until dir == File.dirname(dir)
35
+ return dir if project_root?(dir)
36
+ dir = File.dirname(dir)
37
+ end
38
+ # Fall back to the original directory when no markers were found
39
+ File.expand_path(start_dir)
40
+ end
41
+
31
42
  def self.project_root?(dir = Dir.pwd)
32
43
  File.exist?(File.join(dir, ".git")) ||
33
44
  File.exist?(File.join(dir, "package.json")) ||
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.32.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")