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
@@ -3,23 +3,108 @@
3
3
  module Aidp
4
4
  # Detect basic project tooling to seed work loop test & lint commands.
5
5
  # Lightweight heuristic pass – prefers safety over guessing incorrectly.
6
+ # Provides framework-aware command suggestions with optimal flags for output filtering.
6
7
  class ToolingDetector
7
8
  DETECTORS = [
8
9
  :ruby_bundle,
9
10
  :rspec,
11
+ :minitest,
10
12
  :ruby_standardrb,
11
13
  :node_jest,
12
14
  :node_mocha,
13
15
  :node_eslint,
14
- :python_pytest
16
+ :python_pytest,
17
+ :python_ruff
15
18
  ].freeze
16
19
 
17
- Result = Struct.new(:test_commands, :lint_commands, keyword_init: true)
20
+ # Framework identifiers for output filtering
21
+ FRAMEWORKS = {
22
+ rspec: :rspec,
23
+ minitest: :minitest,
24
+ jest: :jest,
25
+ mocha: :jest, # Mocha uses similar output format
26
+ pytest: :pytest
27
+ }.freeze
28
+
29
+ # Enhanced result with framework information
30
+ Result = Struct.new(:test_commands, :lint_commands, :formatter_commands,
31
+ :frameworks, keyword_init: true) do
32
+ # Get test commands with their detected framework
33
+ def test_command_frameworks
34
+ @test_command_frameworks ||= {}
35
+ end
36
+
37
+ # Get the framework for a specific command
38
+ def framework_for_command(command)
39
+ test_command_frameworks[command] || :unknown
40
+ end
41
+ end
42
+
43
+ # Information about a detected command
44
+ CommandInfo = Struct.new(:command, :framework, :flags, keyword_init: true)
18
45
 
19
46
  def self.detect(root = Dir.pwd)
20
47
  new(root).detect
21
48
  end
22
49
 
50
+ # Detect framework from a command string
51
+ # @param command [String] Command to analyze
52
+ # @return [Symbol] Framework identifier (:rspec, :minitest, :jest, :pytest, :unknown)
53
+ def self.framework_from_command(command)
54
+ return :unknown unless command.is_a?(String)
55
+
56
+ case command.downcase
57
+ when /\brspec\b/
58
+ :rspec
59
+ when /\bminitest\b/, /\bruby.*test/, /\brake test\b/
60
+ :minitest
61
+ when /\bjest\b/, /\bmocha\b/
62
+ :jest
63
+ when /\bpytest\b/
64
+ :pytest
65
+ else
66
+ :unknown
67
+ end
68
+ end
69
+
70
+ # Get recommended command flags for better output filtering
71
+ # @param framework [Symbol] Framework identifier
72
+ # @return [Hash] Recommended flags for different verbosity modes
73
+ def self.recommended_flags(framework)
74
+ case framework
75
+ when :rspec
76
+ {
77
+ standard: "--format progress",
78
+ verbose: "--format documentation",
79
+ failures_only: "--format failures --format progress"
80
+ }
81
+ when :minitest
82
+ {
83
+ standard: "",
84
+ verbose: "-v",
85
+ failures_only: ""
86
+ }
87
+ when :jest
88
+ {
89
+ standard: "",
90
+ verbose: "--verbose",
91
+ failures_only: "--reporters=default --silent=false"
92
+ }
93
+ when :pytest
94
+ {
95
+ standard: "-q",
96
+ verbose: "-v",
97
+ failures_only: "-q --tb=short"
98
+ }
99
+ else
100
+ {
101
+ standard: "",
102
+ verbose: "",
103
+ failures_only: ""
104
+ }
105
+ end
106
+ end
107
+
23
108
  def initialize(root = Dir.pwd)
24
109
  @root = root
25
110
  end
@@ -27,34 +112,143 @@ module Aidp
27
112
  def detect
28
113
  tests = []
29
114
  linters = []
115
+ formatters = []
116
+ frameworks = {}
117
+
118
+ detect_ruby_tools(tests, linters, formatters, frameworks)
119
+ detect_node_tools(tests, linters, formatters, frameworks)
120
+ detect_python_tools(tests, linters, formatters, frameworks)
121
+
122
+ result = Result.new(
123
+ test_commands: tests.uniq,
124
+ lint_commands: linters.uniq,
125
+ formatter_commands: formatters.uniq,
126
+ frameworks: frameworks
127
+ )
128
+
129
+ # Store framework mappings in the result
130
+ frameworks.each do |cmd, framework|
131
+ result.test_command_frameworks[cmd] = framework
132
+ end
133
+
134
+ result
135
+ end
136
+
137
+ # Get detailed command information including framework and suggested flags
138
+ # @return [Array<CommandInfo>] Detailed information about detected commands
139
+ def detect_with_details
140
+ commands = []
30
141
 
31
142
  if ruby_project?
32
- tests << bundle_prefix("rspec") if rspec?
33
- linters << bundle_prefix("standardrb") if standard_rb?
143
+ if rspec?
144
+ commands << CommandInfo.new(
145
+ command: bundle_prefix("rspec"),
146
+ framework: :rspec,
147
+ flags: self.class.recommended_flags(:rspec)
148
+ )
149
+ end
150
+
151
+ if minitest?
152
+ commands << CommandInfo.new(
153
+ command: bundle_prefix("ruby -Itest test"),
154
+ framework: :minitest,
155
+ flags: self.class.recommended_flags(:minitest)
156
+ )
157
+ end
34
158
  end
35
159
 
36
160
  if node_project?
37
- tests << npm_or_yarn("test") if package_script?("test")
38
- %w[lint eslint].each do |script|
39
- if package_script?(script)
40
- linters << npm_or_yarn(script)
41
- break
42
- end
161
+ if jest?
162
+ commands << CommandInfo.new(
163
+ command: npm_or_yarn("test"),
164
+ framework: :jest,
165
+ flags: self.class.recommended_flags(:jest)
166
+ )
43
167
  end
44
168
  end
45
169
 
46
170
  if python_pytest?
47
- tests << "pytest -q"
171
+ commands << CommandInfo.new(
172
+ command: "pytest",
173
+ framework: :pytest,
174
+ flags: self.class.recommended_flags(:pytest)
175
+ )
48
176
  end
49
177
 
50
- Result.new(
51
- test_commands: tests.uniq,
52
- lint_commands: linters.uniq
53
- )
178
+ commands
54
179
  end
55
180
 
56
181
  private
57
182
 
183
+ def detect_ruby_tools(tests, linters, formatters, frameworks)
184
+ return unless ruby_project?
185
+
186
+ if rspec?
187
+ cmd = bundle_prefix("rspec")
188
+ tests << cmd
189
+ frameworks[cmd] = :rspec
190
+ end
191
+
192
+ if minitest?
193
+ cmd = bundle_prefix("ruby -Itest test")
194
+ tests << cmd
195
+ frameworks[cmd] = :minitest
196
+ end
197
+
198
+ if standard_rb?
199
+ linters << bundle_prefix("standardrb")
200
+ formatters << bundle_prefix("standardrb --fix")
201
+ end
202
+
203
+ if rubocop?
204
+ linters << bundle_prefix("rubocop") unless standard_rb?
205
+ formatters << bundle_prefix("rubocop -A") unless standard_rb?
206
+ end
207
+ end
208
+
209
+ def detect_node_tools(tests, linters, formatters, frameworks)
210
+ return unless node_project?
211
+
212
+ if jest?
213
+ cmd = npm_or_yarn("test")
214
+ tests << cmd
215
+ frameworks[cmd] = :jest
216
+ elsif package_script?("test")
217
+ tests << npm_or_yarn("test")
218
+ end
219
+
220
+ %w[lint eslint].each do |script|
221
+ if package_script?(script)
222
+ linters << npm_or_yarn(script)
223
+ break
224
+ end
225
+ end
226
+
227
+ %w[format prettier].each do |script|
228
+ if package_script?(script)
229
+ formatters << npm_or_yarn(script)
230
+ break
231
+ end
232
+ end
233
+ end
234
+
235
+ def detect_python_tools(tests, linters, formatters, frameworks)
236
+ if python_pytest?
237
+ cmd = "pytest -q"
238
+ tests << cmd
239
+ frameworks[cmd] = :pytest
240
+ end
241
+
242
+ if python_ruff?
243
+ linters << "ruff check ."
244
+ formatters << "ruff format ."
245
+ elsif python_flake8?
246
+ linters << "flake8"
247
+ end
248
+
249
+ formatters << "black ." if python_black?
250
+ end
251
+
58
252
  def bundle_prefix(cmd)
59
253
  File.exist?(File.join(@root, "Gemfile")) ? "bundle exec #{cmd}" : cmd
60
254
  end
@@ -72,6 +266,24 @@ module Aidp
72
266
  end
73
267
  end
74
268
 
269
+ def minitest?
270
+ test_dir = File.join(@root, "test")
271
+ return false unless File.exist?(test_dir)
272
+
273
+ # Check for minitest in Gemfile or test files
274
+ gemfile_has_minitest = begin
275
+ File.readlines(File.join(@root, "Gemfile")).grep(/minitest/).any?
276
+ rescue
277
+ false
278
+ end
279
+
280
+ return true if gemfile_has_minitest
281
+
282
+ # Check for test files that use minitest
283
+ test_files = Dir.glob(File.join(test_dir, "**", "*_test.rb"))
284
+ test_files.any?
285
+ end
286
+
75
287
  def standard_rb?
76
288
  File.exist?(File.join(@root, "Gemfile")) &&
77
289
  begin
@@ -81,6 +293,16 @@ module Aidp
81
293
  end
82
294
  end
83
295
 
296
+ def rubocop?
297
+ File.exist?(File.join(@root, ".rubocop.yml")) ||
298
+ (File.exist?(File.join(@root, "Gemfile")) &&
299
+ begin
300
+ File.readlines(File.join(@root, "Gemfile")).grep(/rubocop/).any?
301
+ rescue
302
+ false
303
+ end)
304
+ end
305
+
84
306
  def package_json
85
307
  @package_json ||= begin
86
308
  path = File.join(@root, "package.json")
@@ -107,9 +329,54 @@ module Aidp
107
329
  end
108
330
  end
109
331
 
332
+ def jest?
333
+ return false unless node_project?
334
+
335
+ # Check for jest in dependencies or devDependencies
336
+ deps = package_json&.dig("dependencies") || {}
337
+ dev_deps = package_json&.dig("devDependencies") || {}
338
+
339
+ deps.key?("jest") || dev_deps.key?("jest") ||
340
+ package_json&.dig("scripts", "test")&.include?("jest")
341
+ end
342
+
110
343
  def python_pytest?
111
344
  Dir.glob(File.join(@root, "**", "pytest.ini")).any? ||
112
- Dir.glob(File.join(@root, "**", "conftest.py")).any?
345
+ Dir.glob(File.join(@root, "**", "conftest.py")).any? ||
346
+ (File.exist?(File.join(@root, "pyproject.toml")) &&
347
+ begin
348
+ File.read(File.join(@root, "pyproject.toml")).include?("pytest")
349
+ rescue
350
+ false
351
+ end)
352
+ end
353
+
354
+ def python_ruff?
355
+ File.exist?(File.join(@root, "pyproject.toml")) &&
356
+ begin
357
+ File.read(File.join(@root, "pyproject.toml")).include?("[tool.ruff]")
358
+ rescue
359
+ false
360
+ end
361
+ end
362
+
363
+ def python_flake8?
364
+ File.exist?(File.join(@root, ".flake8")) ||
365
+ File.exist?(File.join(@root, "setup.cfg")) &&
366
+ begin
367
+ File.read(File.join(@root, "setup.cfg")).include?("[flake8]")
368
+ rescue
369
+ false
370
+ end
371
+ end
372
+
373
+ def python_black?
374
+ File.exist?(File.join(@root, "pyproject.toml")) &&
375
+ begin
376
+ File.read(File.join(@root, "pyproject.toml")).include?("[tool.black]")
377
+ rescue
378
+ false
379
+ end
113
380
  end
114
381
  end
115
382
  end
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.32.0"
4
+ VERSION = "0.34.0"
5
5
  end
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../message_display"
4
+
5
+ module Aidp
6
+ module Watch
7
+ # Automatically merges sub-issue PRs when CI passes and conditions are met.
8
+ # Never auto-merges parent PRs - those require human review.
9
+ class AutoMerger
10
+ include Aidp::MessageDisplay
11
+
12
+ # Labels that indicate PR type
13
+ PARENT_PR_LABEL = "aidp-parent-pr"
14
+ SUB_PR_LABEL = "aidp-sub-pr"
15
+
16
+ # Default configuration
17
+ DEFAULT_CONFIG = {
18
+ enabled: true,
19
+ sub_issue_prs_only: true,
20
+ require_ci_success: true,
21
+ require_reviews: 0,
22
+ merge_method: "squash",
23
+ delete_branch: true
24
+ }.freeze
25
+
26
+ attr_reader :repository_client, :state_store
27
+
28
+ def initialize(repository_client:, state_store:, config: {})
29
+ @repository_client = repository_client
30
+ @state_store = state_store
31
+ @config = DEFAULT_CONFIG.merge(config)
32
+ end
33
+
34
+ # Check if a PR can be auto-merged
35
+ # @param pr_number [Integer] The PR number
36
+ # @return [Hash] Result with :can_merge flag and :reason
37
+ def can_auto_merge?(pr_number)
38
+ Aidp.log_debug("auto_merger", "checking_can_auto_merge", pr_number: pr_number)
39
+
40
+ return {can_merge: false, reason: "Auto-merge is disabled"} unless @config[:enabled]
41
+
42
+ # Fetch PR details
43
+ pr = begin
44
+ @repository_client.fetch_pull_request(pr_number)
45
+ rescue => e
46
+ Aidp.log_error("auto_merger", "Failed to fetch PR", pr_number: pr_number, error: e.message)
47
+ return {can_merge: false, reason: "Failed to fetch PR: #{e.message}"}
48
+ end
49
+
50
+ # Check if it's a parent PR (never auto-merge)
51
+ if pr[:labels].include?(PARENT_PR_LABEL)
52
+ Aidp.log_debug("auto_merger", "skipping_parent_pr", pr_number: pr_number)
53
+ return {can_merge: false, reason: "Parent PRs require human review"}
54
+ end
55
+
56
+ # Check if sub-PRs only mode requires the sub-PR label
57
+ if @config[:sub_issue_prs_only] && !pr[:labels].include?(SUB_PR_LABEL)
58
+ Aidp.log_debug("auto_merger", "not_a_sub_pr", pr_number: pr_number)
59
+ return {can_merge: false, reason: "Not a sub-issue PR (missing #{SUB_PR_LABEL} label)"}
60
+ end
61
+
62
+ # Check PR state
63
+ unless pr[:state] == "open" || pr[:state] == "OPEN"
64
+ return {can_merge: false, reason: "PR is not open (state: #{pr[:state]})"}
65
+ end
66
+
67
+ # Check mergeability
68
+ if pr[:mergeable] == false
69
+ return {can_merge: false, reason: "PR has merge conflicts"}
70
+ end
71
+
72
+ # Check CI status
73
+ if @config[:require_ci_success]
74
+ ci_status = @repository_client.fetch_ci_status(pr_number)
75
+ unless ci_status[:state] == "success"
76
+ Aidp.log_debug("auto_merger", "ci_not_passed",
77
+ pr_number: pr_number, ci_state: ci_status[:state])
78
+ return {can_merge: false, reason: "CI has not passed (status: #{ci_status[:state]})"}
79
+ end
80
+ end
81
+
82
+ # All checks passed
83
+ Aidp.log_debug("auto_merger", "can_auto_merge", pr_number: pr_number)
84
+ {can_merge: true, reason: "All merge conditions met"}
85
+ end
86
+
87
+ # Attempt to merge a PR
88
+ # @param pr_number [Integer] The PR number
89
+ # @return [Hash] Result with :success flag, :reason, and optional :merge_sha
90
+ def merge_pr(pr_number)
91
+ Aidp.log_debug("auto_merger", "attempting_merge", pr_number: pr_number)
92
+
93
+ # Verify can merge
94
+ eligibility = can_auto_merge?(pr_number)
95
+ unless eligibility[:can_merge]
96
+ return {success: false, reason: eligibility[:reason]}
97
+ end
98
+
99
+ begin
100
+ result = @repository_client.merge_pull_request(
101
+ pr_number,
102
+ merge_method: @config[:merge_method]
103
+ )
104
+
105
+ Aidp.log_info("auto_merger", "pr_merged",
106
+ pr_number: pr_number, merge_method: @config[:merge_method])
107
+ display_message("✅ Auto-merged PR ##{pr_number}", type: :success)
108
+
109
+ # Post comment about auto-merge
110
+ post_merge_comment(pr_number)
111
+
112
+ # Update parent issue/PR if this was a sub-issue PR
113
+ update_parent_after_merge(pr_number)
114
+
115
+ {success: true, reason: "Successfully merged", result: result}
116
+ rescue => e
117
+ Aidp.log_error("auto_merger", "merge_failed",
118
+ pr_number: pr_number, error: e.message)
119
+ display_message("❌ Failed to auto-merge PR ##{pr_number}: #{e.message}", type: :error)
120
+ {success: false, reason: "Merge failed: #{e.message}"}
121
+ end
122
+ end
123
+
124
+ # Process all eligible PRs for auto-merge
125
+ # @param prs [Array<Hash>] Array of PR data with :number keys
126
+ # @return [Hash] Summary with :merged, :skipped, :failed counts
127
+ def process_auto_merge_candidates(prs)
128
+ Aidp.log_debug("auto_merger", "processing_candidates", count: prs.size)
129
+
130
+ merged = 0
131
+ skipped = 0
132
+ failed = 0
133
+
134
+ prs.each do |pr|
135
+ pr_number = pr[:number]
136
+
137
+ eligibility = can_auto_merge?(pr_number)
138
+ unless eligibility[:can_merge]
139
+ Aidp.log_debug("auto_merger", "skipping_pr",
140
+ pr_number: pr_number, reason: eligibility[:reason])
141
+ skipped += 1
142
+ next
143
+ end
144
+
145
+ result = merge_pr(pr_number)
146
+ if result[:success]
147
+ merged += 1
148
+ else
149
+ failed += 1
150
+ end
151
+ end
152
+
153
+ summary = {merged: merged, skipped: skipped, failed: failed}
154
+ Aidp.log_info("auto_merger", "processing_complete", **summary)
155
+ display_message("🔀 Auto-merge: #{merged} merged, #{skipped} skipped, #{failed} failed",
156
+ type: :info)
157
+ summary
158
+ end
159
+
160
+ # List all PRs with the sub-PR label that are candidates for auto-merge
161
+ # @return [Array<Hash>] PRs that might be eligible for auto-merge
162
+ def list_sub_pr_candidates
163
+ @repository_client.list_pull_requests(labels: [SUB_PR_LABEL], state: "open")
164
+ rescue => e
165
+ Aidp.log_error("auto_merger", "Failed to list sub-PR candidates", error: e.message)
166
+ []
167
+ end
168
+
169
+ private
170
+
171
+ def post_merge_comment(pr_number)
172
+ comment = <<~COMMENT
173
+ ✅ This PR was automatically merged by AIDP after CI passed.
174
+
175
+ Merge method: `#{@config[:merge_method]}`
176
+
177
+ ---
178
+ _Sub-issue PRs are automatically merged when CI passes. Parent PRs always require human review._
179
+ COMMENT
180
+
181
+ begin
182
+ @repository_client.post_comment(pr_number, comment)
183
+ rescue => e
184
+ Aidp.log_warn("auto_merger", "Failed to post merge comment",
185
+ pr_number: pr_number, error: e.message)
186
+ end
187
+ end
188
+
189
+ def update_parent_after_merge(pr_number)
190
+ # Find the parent issue for this sub-PR
191
+ # The sub-PR should target the parent's branch, so we can identify it
192
+
193
+ # First, check if we have hierarchy data
194
+ build_data = @state_store.find_build_by_pr(pr_number)
195
+ return unless build_data
196
+
197
+ issue_number = build_data[:issue_number]
198
+ parent_number = @state_store.parent_issue(issue_number)
199
+ return unless parent_number
200
+
201
+ Aidp.log_debug("auto_merger", "updating_parent_after_merge",
202
+ sub_issue: issue_number, parent: parent_number)
203
+
204
+ # Check if all sub-issues are now complete
205
+ sub_issues = @state_store.sub_issues(parent_number)
206
+ all_complete = sub_issues.all? do |sub_number|
207
+ sub_build = @state_store.workstream_for_issue(sub_number)
208
+ sub_build && sub_build[:status] == "completed"
209
+ end
210
+
211
+ if all_complete
212
+ notify_parent_ready_for_review(parent_number)
213
+ end
214
+ rescue => e
215
+ Aidp.log_warn("auto_merger", "Failed to update parent after merge",
216
+ pr_number: pr_number, error: e.message)
217
+ end
218
+
219
+ def notify_parent_ready_for_review(parent_number)
220
+ Aidp.log_info("auto_merger", "all_sub_issues_complete", parent: parent_number)
221
+
222
+ comment = <<~COMMENT
223
+ 🎉 All sub-issue PRs have been merged!
224
+
225
+ The parent PR is now ready for final review and merge to main.
226
+
227
+ ### Sub-Issues Completed
228
+ #{format_sub_issues_list(parent_number)}
229
+
230
+ **Next Steps:**
231
+ 1. Review the combined changes in the parent PR
232
+ 2. Ensure all integration tests pass
233
+ 3. Merge the parent PR manually
234
+
235
+ ---
236
+ _Parent PRs are never auto-merged and require human review._
237
+ COMMENT
238
+
239
+ begin
240
+ @repository_client.post_comment(parent_number, comment)
241
+ display_message("📋 Notified parent issue ##{parent_number} that all sub-PRs are merged",
242
+ type: :success)
243
+
244
+ # Mark the parent PR as ready for review if it's still draft
245
+ parent_build = @state_store.workstream_for_issue(parent_number)
246
+ if parent_build && parent_build[:pr_url]
247
+ pr_number = parent_build[:pr_url].split("/").last.to_i
248
+ begin
249
+ @repository_client.mark_pr_ready_for_review(pr_number)
250
+ display_message("✅ Marked parent PR ##{pr_number} as ready for review", type: :success)
251
+ rescue => e
252
+ Aidp.log_warn("auto_merger", "Failed to mark parent PR ready",
253
+ pr_number: pr_number, error: e.message)
254
+ end
255
+ end
256
+ rescue => e
257
+ Aidp.log_warn("auto_merger", "Failed to notify parent",
258
+ parent: parent_number, error: e.message)
259
+ end
260
+ end
261
+
262
+ def format_sub_issues_list(parent_number)
263
+ sub_issues = @state_store.sub_issues(parent_number)
264
+ return "_No sub-issues found_" if sub_issues.empty?
265
+
266
+ sub_issues.map do |sub_number|
267
+ build = @state_store.workstream_for_issue(sub_number)
268
+ pr_link = build&.dig(:pr_url) || "No PR"
269
+ "- ##{sub_number}: #{pr_link}"
270
+ end.join("\n")
271
+ end
272
+ end
273
+ end
274
+ end