aidp 0.32.0 → 0.33.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/lib/aidp/analyze/feature_analyzer.rb +322 -320
- 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.rb +2 -1
- data/lib/aidp/comment_consolidator.rb +78 -0
- data/lib/aidp/concurrency.rb +0 -3
- data/lib/aidp/config.rb +0 -1
- data/lib/aidp/config_paths.rb +71 -0
- data/lib/aidp/execute/work_loop_runner.rb +324 -15
- data/lib/aidp/harness/ai_filter_factory.rb +285 -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/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_manager.rb +90 -2
- data/lib/aidp/harness/runner.rb +0 -11
- data/lib/aidp/harness/test_runner.rb +179 -41
- data/lib/aidp/harness/thinking_depth_manager.rb +16 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +0 -2
- data/lib/aidp/loader.rb +195 -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 +2 -2
- data/lib/aidp/provider_manager.rb +1 -7
- data/lib/aidp/setup/wizard.rb +279 -9
- 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/change_request_processor.rb +152 -14
- data/lib/aidp/watch/repository_client.rb +41 -0
- data/lib/aidp/watch/runner.rb +29 -18
- data/lib/aidp/watch.rb +5 -7
- data/lib/aidp/workstream_cleanup.rb +0 -2
- data/lib/aidp/workstream_executor.rb +0 -4
- data/lib/aidp/worktree.rb +0 -1
- data/lib/aidp.rb +21 -106
- metadata +72 -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
|
@@ -40,6 +40,12 @@ module Aidp
|
|
|
40
40
|
@project_dir = project_dir
|
|
41
41
|
@verbose = verbose
|
|
42
42
|
|
|
43
|
+
# Log initialization details
|
|
44
|
+
Aidp.log_debug("change_request_processor", "initializing",
|
|
45
|
+
provider_name: provider_name,
|
|
46
|
+
project_dir: project_dir,
|
|
47
|
+
verbose: verbose)
|
|
48
|
+
|
|
43
49
|
# Initialize verifier
|
|
44
50
|
@verifier = ImplementationVerifier.new(
|
|
45
51
|
repository_client: repository_client,
|
|
@@ -50,6 +56,11 @@ module Aidp
|
|
|
50
56
|
@change_request_label = label_config[:change_request_trigger] || label_config["change_request_trigger"] || DEFAULT_CHANGE_REQUEST_LABEL
|
|
51
57
|
@needs_input_label = label_config[:needs_input] || label_config["needs_input"] || DEFAULT_NEEDS_INPUT_LABEL
|
|
52
58
|
|
|
59
|
+
# Log label details
|
|
60
|
+
Aidp.log_debug("change_request_processor", "label_configuration",
|
|
61
|
+
change_request_label: @change_request_label,
|
|
62
|
+
needs_input_label: @needs_input_label)
|
|
63
|
+
|
|
53
64
|
# Load change request configuration
|
|
54
65
|
@config = {
|
|
55
66
|
enabled: true,
|
|
@@ -61,9 +72,18 @@ module Aidp
|
|
|
61
72
|
allow_large_pr_worktree_bypass: true # Default to always using worktree for large PRs
|
|
62
73
|
}.merge(symbolize_keys(change_request_config))
|
|
63
74
|
|
|
75
|
+
# Log configuration details
|
|
76
|
+
Aidp.log_debug("change_request_processor", "change_request_config",
|
|
77
|
+
config: @config.transform_values { |v| v.is_a?(Proc) ? "Proc" : v })
|
|
78
|
+
|
|
64
79
|
# Load safety configuration
|
|
65
80
|
@safety_config = safety_config
|
|
66
81
|
@author_allowlist = Array(@safety_config[:author_allowlist] || @safety_config["author_allowlist"])
|
|
82
|
+
|
|
83
|
+
# Log safety configuration
|
|
84
|
+
Aidp.log_debug("change_request_processor", "safety_configuration",
|
|
85
|
+
author_allowlist: @author_allowlist,
|
|
86
|
+
allowlist_count: @author_allowlist.length)
|
|
67
87
|
end
|
|
68
88
|
|
|
69
89
|
def process(pr)
|
|
@@ -142,15 +162,29 @@ module Aidp
|
|
|
142
162
|
def filter_authorized_comments(comments, pr_data)
|
|
143
163
|
# If allowlist is empty (for private repos), consider PR author and all commenters
|
|
144
164
|
# For public repos, enforce allowlist
|
|
165
|
+
Aidp.log_debug("change_request_processor", "filtering_authorized_comments",
|
|
166
|
+
total_comments: comments.length,
|
|
167
|
+
allowlist_count: @author_allowlist.length,
|
|
168
|
+
is_private_repo: @author_allowlist.empty?)
|
|
169
|
+
|
|
145
170
|
if @author_allowlist.empty?
|
|
146
171
|
# Private repo: trust all comments from PR participants
|
|
172
|
+
Aidp.log_debug("change_request_processor", "private_repo_comments_allowed",
|
|
173
|
+
comments_allowed: comments.length)
|
|
147
174
|
comments
|
|
148
175
|
else
|
|
149
176
|
# Public repo: only allow comments from allowlisted users
|
|
150
|
-
comments.select do |comment|
|
|
177
|
+
authorized_comments = comments.select do |comment|
|
|
151
178
|
author = comment[:author]
|
|
152
179
|
@author_allowlist.include?(author)
|
|
153
180
|
end
|
|
181
|
+
|
|
182
|
+
Aidp.log_debug("change_request_processor", "public_repo_comment_filtering",
|
|
183
|
+
total_comments: comments.length,
|
|
184
|
+
authorized_comments: authorized_comments.length,
|
|
185
|
+
allowed_authors: authorized_comments.map { |c| c[:author] })
|
|
186
|
+
|
|
187
|
+
authorized_comments
|
|
154
188
|
end
|
|
155
189
|
end
|
|
156
190
|
|
|
@@ -395,14 +429,83 @@ module Aidp
|
|
|
395
429
|
def create_worktree_for_pr(pr_data)
|
|
396
430
|
head_ref = pr_data[:head_ref]
|
|
397
431
|
pr_number = pr_data[:number]
|
|
398
|
-
slug = "pr-#{pr_number}-change-requests"
|
|
399
432
|
|
|
400
|
-
|
|
433
|
+
# Configure slug and worktree strategy
|
|
434
|
+
slug = pr_data.fetch(:worktree_slug, "pr-#{pr_number}-change-requests")
|
|
435
|
+
strategy = @config.fetch(:worktree_strategy, "auto")
|
|
436
|
+
|
|
437
|
+
display_message("🌿 Preparing worktree for PR ##{pr_number}: #{head_ref} (Strategy: #{strategy})", type: :info)
|
|
401
438
|
|
|
439
|
+
# Pre-create setup: fetch latest refs
|
|
402
440
|
Dir.chdir(@project_dir) do
|
|
403
441
|
run_git(%w[fetch origin], allow_failure: true)
|
|
404
442
|
end
|
|
405
443
|
|
|
444
|
+
# Worktree creation strategy
|
|
445
|
+
worktree_path =
|
|
446
|
+
case strategy
|
|
447
|
+
when "always_create"
|
|
448
|
+
create_fresh_worktree(pr_data, slug)
|
|
449
|
+
when "reuse_only"
|
|
450
|
+
find_existing_worktree(pr_data, slug)
|
|
451
|
+
else # 'auto' or default
|
|
452
|
+
find_existing_worktree(pr_data, slug) || create_fresh_worktree(pr_data, slug)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
Aidp.log_debug(
|
|
456
|
+
"change_request_processor",
|
|
457
|
+
"worktree_resolved",
|
|
458
|
+
pr_number: pr_number,
|
|
459
|
+
branch: head_ref,
|
|
460
|
+
path: worktree_path,
|
|
461
|
+
strategy: strategy
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
display_message("✅ Worktree available at #{worktree_path}", type: :success)
|
|
465
|
+
worktree_path
|
|
466
|
+
rescue => e
|
|
467
|
+
Aidp.log_error(
|
|
468
|
+
"change_request_processor",
|
|
469
|
+
"worktree_creation_failed",
|
|
470
|
+
pr_number: pr_number,
|
|
471
|
+
error: e.message,
|
|
472
|
+
backtrace: e.backtrace&.first(5)
|
|
473
|
+
)
|
|
474
|
+
display_message("❌ Failed to create worktree: #{e.message}", type: :error)
|
|
475
|
+
raise
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
private
|
|
479
|
+
|
|
480
|
+
def find_existing_worktree(pr_data, slug)
|
|
481
|
+
head_ref = pr_data[:head_ref]
|
|
482
|
+
pr_number = pr_data[:number]
|
|
483
|
+
|
|
484
|
+
# First check for existing worktree by branch
|
|
485
|
+
existing = Aidp::Worktree.find_by_branch(branch: head_ref, project_dir: @project_dir)
|
|
486
|
+
return existing[:path] if existing && existing[:active]
|
|
487
|
+
|
|
488
|
+
# If no branch-specific worktree, look for PR-specific worktree
|
|
489
|
+
pr_worktrees = Aidp::Worktree.list(project_dir: @project_dir)
|
|
490
|
+
pr_specific_worktree = pr_worktrees.find do |w|
|
|
491
|
+
w[:slug]&.include?("pr-#{pr_number}")
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
pr_specific_worktree ? pr_specific_worktree[:path] : nil
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def create_fresh_worktree(pr_data, slug)
|
|
498
|
+
head_ref = pr_data[:head_ref]
|
|
499
|
+
pr_number = pr_data[:number]
|
|
500
|
+
|
|
501
|
+
Aidp.log_debug(
|
|
502
|
+
"change_request_processor",
|
|
503
|
+
"creating_new_worktree",
|
|
504
|
+
pr_number: pr_number,
|
|
505
|
+
branch: head_ref,
|
|
506
|
+
slug: slug
|
|
507
|
+
)
|
|
508
|
+
|
|
406
509
|
result = Aidp::Worktree.create(
|
|
407
510
|
slug: slug,
|
|
408
511
|
project_dir: @project_dir,
|
|
@@ -410,11 +513,7 @@ module Aidp
|
|
|
410
513
|
base_branch: pr_data[:base_ref]
|
|
411
514
|
)
|
|
412
515
|
|
|
413
|
-
|
|
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
|
|
516
|
+
result[:path]
|
|
418
517
|
end
|
|
419
518
|
|
|
420
519
|
def apply_changes(changes)
|
|
@@ -571,22 +670,42 @@ module Aidp
|
|
|
571
670
|
end
|
|
572
671
|
|
|
573
672
|
def handle_incomplete_implementation(pr:, analysis:, verification_result:)
|
|
673
|
+
Aidp.log_debug("change_request_processor", "start_incomplete_implementation_handling",
|
|
674
|
+
pr_number: pr[:number],
|
|
675
|
+
verification_result: {
|
|
676
|
+
missing_items_count: verification_result[:missing_items]&.length || 0,
|
|
677
|
+
additional_work_count: verification_result[:additional_work]&.length || 0
|
|
678
|
+
})
|
|
679
|
+
|
|
574
680
|
display_message("⚠️ Implementation incomplete; creating follow-up tasks.", type: :warn)
|
|
575
681
|
|
|
576
682
|
# Create tasks for missing requirements
|
|
577
683
|
if verification_result[:additional_work] && !verification_result[:additional_work].empty?
|
|
684
|
+
Aidp.log_debug("change_request_processor", "preparing_follow_up_tasks",
|
|
685
|
+
pr_number: pr[:number],
|
|
686
|
+
additional_work_tasks_count: verification_result[:additional_work].length)
|
|
578
687
|
create_follow_up_tasks(@project_dir, verification_result[:additional_work])
|
|
579
688
|
end
|
|
580
689
|
|
|
581
690
|
# Record state but do not post a separate comment
|
|
582
691
|
# (verification details will be included in the next summary comment)
|
|
583
|
-
|
|
692
|
+
state_record = {
|
|
584
693
|
status: "incomplete_implementation",
|
|
585
694
|
timestamp: Time.now.utc.iso8601,
|
|
586
695
|
verification_reasons: verification_result[:reasons],
|
|
587
696
|
missing_items: verification_result[:missing_items],
|
|
588
697
|
additional_work: verification_result[:additional_work]
|
|
589
|
-
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
# Log the details of the state record before storing
|
|
701
|
+
Aidp.log_debug("change_request_processor", "recording_incomplete_implementation_state",
|
|
702
|
+
pr_number: pr[:number],
|
|
703
|
+
status: state_record[:status],
|
|
704
|
+
verification_reasons_count: state_record[:verification_reasons]&.length || 0,
|
|
705
|
+
missing_items_count: state_record[:missing_items]&.length || 0,
|
|
706
|
+
additional_work_count: state_record[:additional_work]&.length || 0)
|
|
707
|
+
|
|
708
|
+
@state_store.record_change_request(pr[:number], state_record)
|
|
590
709
|
|
|
591
710
|
display_message("📝 Recorded incomplete implementation status for PR ##{pr[:number]}", type: :info)
|
|
592
711
|
|
|
@@ -603,36 +722,55 @@ module Aidp
|
|
|
603
722
|
def create_follow_up_tasks(working_dir, additional_work)
|
|
604
723
|
return if additional_work.nil? || additional_work.empty?
|
|
605
724
|
|
|
725
|
+
Aidp.log_debug("change_request_processor", "start_creating_follow_up_tasks",
|
|
726
|
+
working_dir: working_dir,
|
|
727
|
+
additional_work_tasks_count: additional_work.length)
|
|
728
|
+
|
|
606
729
|
tasklist_file = File.join(working_dir, ".aidp", "tasklist.jsonl")
|
|
607
730
|
FileUtils.mkdir_p(File.dirname(tasklist_file))
|
|
608
731
|
|
|
609
732
|
require_relative "../execute/persistent_tasklist"
|
|
610
733
|
tasklist = Aidp::Execute::PersistentTasklist.new(working_dir)
|
|
611
734
|
|
|
735
|
+
tasks_created = []
|
|
612
736
|
additional_work.each do |task_description|
|
|
613
|
-
tasklist.create(
|
|
737
|
+
task = tasklist.create(
|
|
614
738
|
description: task_description,
|
|
615
739
|
priority: :high,
|
|
616
740
|
source: "verification"
|
|
617
741
|
)
|
|
742
|
+
tasks_created << task
|
|
618
743
|
end
|
|
619
744
|
|
|
620
745
|
display_message("📝 Created #{additional_work.length} follow-up task(s) for continued work", type: :info)
|
|
621
746
|
|
|
747
|
+
Aidp.log_debug("change_request_processor", "follow_up_tasks_details",
|
|
748
|
+
task_count: tasks_created.length,
|
|
749
|
+
working_dir: working_dir,
|
|
750
|
+
task_descriptions: tasks_created.map(&:description))
|
|
751
|
+
|
|
622
752
|
Aidp.log_info(
|
|
623
753
|
"change_request_processor",
|
|
624
754
|
"created_follow_up_tasks",
|
|
625
|
-
task_count:
|
|
755
|
+
task_count: tasks_created.length,
|
|
626
756
|
working_dir: working_dir
|
|
627
757
|
)
|
|
758
|
+
|
|
759
|
+
tasks_created
|
|
628
760
|
rescue => e
|
|
629
|
-
display_message("⚠️ Failed to create follow-up tasks: #{e.message}", type: :warn)
|
|
630
761
|
Aidp.log_error(
|
|
631
762
|
"change_request_processor",
|
|
632
763
|
"failed_to_create_follow_up_tasks",
|
|
633
764
|
error: e.message,
|
|
634
|
-
|
|
765
|
+
error_class: e.class.name,
|
|
766
|
+
backtrace: e.backtrace&.first(5),
|
|
767
|
+
working_dir: working_dir
|
|
635
768
|
)
|
|
769
|
+
|
|
770
|
+
display_message("⚠️ Failed to create follow-up tasks: #{e.message}", type: :warn)
|
|
771
|
+
|
|
772
|
+
# Return an empty array to indicate failure
|
|
773
|
+
[]
|
|
636
774
|
end
|
|
637
775
|
|
|
638
776
|
def handle_clarification_needed(pr:, analysis:)
|
|
@@ -120,6 +120,47 @@ module Aidp
|
|
|
120
120
|
gh_available? ? fetch_pr_comments_via_gh(number) : fetch_pr_comments_via_api(number)
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# Create or update a categorized comment (e.g., under a header) on an issue.
|
|
124
|
+
# If a comment with the category header exists, either append to it or
|
|
125
|
+
# replace it while archiving the previous content inline.
|
|
126
|
+
def consolidate_category_comment(issue_number, category_header, content, append: false)
|
|
127
|
+
existing_comment = find_comment(issue_number, category_header)
|
|
128
|
+
|
|
129
|
+
if existing_comment.nil?
|
|
130
|
+
Aidp.log_debug("repository_client", "creating_category_comment",
|
|
131
|
+
issue: issue_number,
|
|
132
|
+
header: category_header)
|
|
133
|
+
return post_comment(issue_number, "#{category_header}\n\n#{content}")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
existing_body = existing_comment[:body] || existing_comment["body"] || ""
|
|
137
|
+
content_without_header = existing_body.sub(/\A#{Regexp.escape(category_header)}\s*/, "").strip
|
|
138
|
+
|
|
139
|
+
new_body =
|
|
140
|
+
if append
|
|
141
|
+
Aidp.log_debug("repository_client", "appending_category_comment",
|
|
142
|
+
issue: issue_number,
|
|
143
|
+
header: category_header)
|
|
144
|
+
segments = [category_header, content_without_header, content].reject(&:empty?)
|
|
145
|
+
segments.join("\n\n")
|
|
146
|
+
else
|
|
147
|
+
Aidp.log_debug("repository_client", "replacing_category_comment",
|
|
148
|
+
issue: issue_number,
|
|
149
|
+
header: category_header)
|
|
150
|
+
timestamp = Time.now.utc.iso8601
|
|
151
|
+
archive_marker = "<!-- ARCHIVED_PLAN_START #{timestamp} ARCHIVED_PLAN_END -->"
|
|
152
|
+
[category_header, content, archive_marker, content_without_header].join("\n\n")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
update_comment(existing_comment[:id] || existing_comment["id"], new_body)
|
|
156
|
+
rescue => e
|
|
157
|
+
Aidp.log_error("repository_client", "consolidate_category_comment_failed",
|
|
158
|
+
issue: issue_number,
|
|
159
|
+
header: category_header,
|
|
160
|
+
error: e.message)
|
|
161
|
+
raise "GitHub error: #{e.message}"
|
|
162
|
+
end
|
|
163
|
+
|
|
123
164
|
private
|
|
124
165
|
|
|
125
166
|
# Retry a GitHub CLI operation with exponential backoff
|