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.
- checksums.yaml +4 -4
- data/README.md +35 -0
- data/lib/aidp/analyze/feature_analyzer.rb +322 -320
- data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
- data/lib/aidp/auto_update/coordinator.rb +97 -7
- data/lib/aidp/auto_update.rb +0 -12
- data/lib/aidp/cli/devcontainer_commands.rb +0 -5
- data/lib/aidp/cli/eval_command.rb +399 -0
- data/lib/aidp/cli/harness_command.rb +1 -1
- data/lib/aidp/cli/security_command.rb +416 -0
- data/lib/aidp/cli/tools_command.rb +6 -4
- data/lib/aidp/cli.rb +172 -4
- data/lib/aidp/comment_consolidator.rb +78 -0
- data/lib/aidp/concurrency/exec.rb +3 -0
- data/lib/aidp/concurrency.rb +0 -3
- data/lib/aidp/config.rb +113 -1
- data/lib/aidp/config_paths.rb +91 -0
- data/lib/aidp/daemon/runner.rb +8 -4
- data/lib/aidp/errors.rb +134 -0
- data/lib/aidp/evaluations/context_capture.rb +205 -0
- data/lib/aidp/evaluations/evaluation_record.rb +114 -0
- data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
- data/lib/aidp/evaluations.rb +23 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
- data/lib/aidp/execute/interactive_repl.rb +6 -2
- data/lib/aidp/execute/prompt_evaluator.rb +359 -0
- data/lib/aidp/execute/repl_macros.rb +100 -1
- data/lib/aidp/execute/work_loop_runner.rb +719 -58
- data/lib/aidp/execute/work_loop_state.rb +4 -1
- data/lib/aidp/execute/workflow_selector.rb +3 -0
- data/lib/aidp/harness/ai_decision_engine.rb +79 -0
- data/lib/aidp/harness/ai_filter_factory.rb +285 -0
- data/lib/aidp/harness/capability_registry.rb +2 -0
- data/lib/aidp/harness/condition_detector.rb +3 -0
- data/lib/aidp/harness/config_loader.rb +3 -0
- data/lib/aidp/harness/config_schema.rb +97 -1
- data/lib/aidp/harness/config_validator.rb +1 -1
- data/lib/aidp/harness/configuration.rb +61 -5
- data/lib/aidp/harness/enhanced_runner.rb +14 -11
- data/lib/aidp/harness/error_handler.rb +3 -0
- data/lib/aidp/harness/filter_definition.rb +212 -0
- data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
- data/lib/aidp/harness/output_filter.rb +50 -25
- data/lib/aidp/harness/output_filter_config.rb +129 -0
- data/lib/aidp/harness/provider_factory.rb +3 -0
- data/lib/aidp/harness/provider_manager.rb +96 -2
- data/lib/aidp/harness/runner.rb +5 -12
- data/lib/aidp/harness/state/persistence.rb +3 -0
- data/lib/aidp/harness/state_manager.rb +3 -0
- data/lib/aidp/harness/status_display.rb +28 -20
- data/lib/aidp/harness/test_runner.rb +179 -41
- data/lib/aidp/harness/thinking_depth_manager.rb +44 -28
- data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
- data/lib/aidp/harness/ui/error_handler.rb +3 -0
- data/lib/aidp/harness/ui/job_monitor.rb +4 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +2 -2
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
- data/lib/aidp/harness/user_interface.rb +3 -0
- data/lib/aidp/loader.rb +195 -0
- data/lib/aidp/logger.rb +3 -0
- data/lib/aidp/message_display.rb +31 -0
- data/lib/aidp/metadata/compiler.rb +29 -17
- data/lib/aidp/metadata/query.rb +1 -1
- data/lib/aidp/metadata/scanner.rb +8 -1
- data/lib/aidp/metadata/tool_metadata.rb +13 -13
- data/lib/aidp/metadata/validator.rb +10 -0
- data/lib/aidp/metadata.rb +16 -0
- data/lib/aidp/pr_worktree_manager.rb +20 -8
- data/lib/aidp/provider_manager.rb +4 -7
- data/lib/aidp/providers/base.rb +2 -0
- data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
- data/lib/aidp/security/secrets_proxy.rb +328 -0
- data/lib/aidp/security/secrets_registry.rb +227 -0
- data/lib/aidp/security/trifecta_state.rb +220 -0
- data/lib/aidp/security/watch_mode_handler.rb +306 -0
- data/lib/aidp/security/work_loop_adapter.rb +277 -0
- data/lib/aidp/security.rb +56 -0
- data/lib/aidp/setup/wizard.rb +283 -11
- data/lib/aidp/skills.rb +0 -5
- data/lib/aidp/storage/csv_storage.rb +3 -0
- data/lib/aidp/style_guide/selector.rb +360 -0
- data/lib/aidp/tooling_detector.rb +283 -16
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/auto_merger.rb +274 -0
- data/lib/aidp/watch/auto_pr_processor.rb +125 -7
- data/lib/aidp/watch/build_processor.rb +16 -1
- data/lib/aidp/watch/change_request_processor.rb +682 -150
- data/lib/aidp/watch/ci_fix_processor.rb +262 -4
- data/lib/aidp/watch/feedback_collector.rb +191 -0
- data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
- data/lib/aidp/watch/implementation_verifier.rb +142 -1
- data/lib/aidp/watch/plan_generator.rb +70 -13
- data/lib/aidp/watch/plan_processor.rb +12 -5
- data/lib/aidp/watch/projects_processor.rb +286 -0
- data/lib/aidp/watch/repository_client.rb +871 -22
- data/lib/aidp/watch/review_processor.rb +33 -6
- data/lib/aidp/watch/runner.rb +80 -29
- data/lib/aidp/watch/state_store.rb +233 -0
- data/lib/aidp/watch/sub_issue_creator.rb +221 -0
- data/lib/aidp/watch.rb +5 -7
- data/lib/aidp/workflows/guided_agent.rb +4 -0
- data/lib/aidp/workstream_cleanup.rb +0 -2
- data/lib/aidp/workstream_executor.rb +3 -4
- data/lib/aidp/worktree.rb +61 -12
- data/lib/aidp/worktree_branch_manager.rb +347 -101
- data/lib/aidp.rb +21 -106
- data/templates/implementation/iterative_implementation.md +46 -3
- metadata +91 -36
- 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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
171
|
+
commands << CommandInfo.new(
|
|
172
|
+
command: "pytest",
|
|
173
|
+
framework: :pytest,
|
|
174
|
+
flags: self.class.recommended_flags(:pytest)
|
|
175
|
+
)
|
|
48
176
|
end
|
|
49
177
|
|
|
50
|
-
|
|
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
|
@@ -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
|