aidp 0.25.0 â 0.26.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 +45 -6
- data/lib/aidp/analyze/error_handler.rb +11 -0
- data/lib/aidp/execute/work_loop_runner.rb +225 -55
- data/lib/aidp/harness/config_loader.rb +20 -11
- data/lib/aidp/harness/config_schema.rb +30 -8
- data/lib/aidp/harness/configuration.rb +73 -2
- data/lib/aidp/harness/filter_strategy.rb +45 -0
- data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
- data/lib/aidp/harness/output_filter.rb +136 -0
- data/lib/aidp/harness/provider_manager.rb +18 -3
- data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
- data/lib/aidp/harness/test_runner.rb +165 -27
- data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
- data/lib/aidp/logger.rb +35 -5
- data/lib/aidp/message_display.rb +46 -0
- data/lib/aidp/safe_directory.rb +10 -3
- data/lib/aidp/storage/csv_storage.rb +9 -3
- data/lib/aidp/storage/file_manager.rb +8 -2
- data/lib/aidp/storage/json_storage.rb +9 -3
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +40 -1
- data/lib/aidp/watch/change_request_processor.rb +659 -0
- data/lib/aidp/watch/plan_processor.rb +71 -8
- data/lib/aidp/watch/repository_client.rb +85 -20
- data/lib/aidp/watch/runner.rb +37 -0
- data/lib/aidp/watch/state_store.rb +46 -1
- data/lib/aidp/workstream_executor.rb +5 -2
- data/lib/aidp.rb +4 -0
- data/templates/aidp.yml.example +53 -0
- metadata +6 -1
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
require_relative "../message_display"
|
|
9
|
+
require_relative "../provider_manager"
|
|
10
|
+
require_relative "../harness/config_manager"
|
|
11
|
+
require_relative "../execute/prompt_manager"
|
|
12
|
+
require_relative "../harness/runner"
|
|
13
|
+
require_relative "../harness/state_manager"
|
|
14
|
+
require_relative "../harness/test_runner"
|
|
15
|
+
|
|
16
|
+
module Aidp
|
|
17
|
+
module Watch
|
|
18
|
+
# Handles the aidp-request-changes label trigger by analyzing PR comments
|
|
19
|
+
# and automatically implementing the requested changes.
|
|
20
|
+
class ChangeRequestProcessor
|
|
21
|
+
include Aidp::MessageDisplay
|
|
22
|
+
|
|
23
|
+
# Default label names
|
|
24
|
+
DEFAULT_CHANGE_REQUEST_LABEL = "aidp-request-changes"
|
|
25
|
+
DEFAULT_NEEDS_INPUT_LABEL = "aidp-needs-input"
|
|
26
|
+
|
|
27
|
+
COMMENT_HEADER = "## đ¤ AIDP Change Request"
|
|
28
|
+
MAX_CLARIFICATION_ROUNDS = 3
|
|
29
|
+
|
|
30
|
+
attr_reader :change_request_label, :needs_input_label
|
|
31
|
+
|
|
32
|
+
def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, change_request_config: {}, safety_config: {}, verbose: false)
|
|
33
|
+
@repository_client = repository_client
|
|
34
|
+
@state_store = state_store
|
|
35
|
+
@provider_name = provider_name
|
|
36
|
+
@project_dir = project_dir
|
|
37
|
+
@verbose = verbose
|
|
38
|
+
|
|
39
|
+
# Load label configuration
|
|
40
|
+
@change_request_label = label_config[:change_request_trigger] || label_config["change_request_trigger"] || DEFAULT_CHANGE_REQUEST_LABEL
|
|
41
|
+
@needs_input_label = label_config[:needs_input] || label_config["needs_input"] || DEFAULT_NEEDS_INPUT_LABEL
|
|
42
|
+
|
|
43
|
+
# Load change request configuration
|
|
44
|
+
@config = {
|
|
45
|
+
enabled: true,
|
|
46
|
+
allow_multi_file_edits: true,
|
|
47
|
+
run_tests_before_push: true,
|
|
48
|
+
commit_message_prefix: "aidp: pr-change",
|
|
49
|
+
require_comment_reference: true,
|
|
50
|
+
max_diff_size: 2000
|
|
51
|
+
}.merge(symbolize_keys(change_request_config))
|
|
52
|
+
|
|
53
|
+
# Load safety configuration
|
|
54
|
+
@safety_config = safety_config
|
|
55
|
+
@author_allowlist = Array(@safety_config[:author_allowlist] || @safety_config["author_allowlist"])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def process(pr)
|
|
59
|
+
number = pr[:number]
|
|
60
|
+
|
|
61
|
+
unless @config[:enabled]
|
|
62
|
+
display_message("âšī¸ PR change requests are disabled in configuration. Skipping PR ##{number}.", type: :muted)
|
|
63
|
+
return
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check clarification round limit
|
|
67
|
+
existing_data = @state_store.change_request_data(number)
|
|
68
|
+
if existing_data && existing_data["clarification_count"].to_i >= MAX_CLARIFICATION_ROUNDS
|
|
69
|
+
display_message("â ī¸ Max clarification rounds (#{MAX_CLARIFICATION_ROUNDS}) reached for PR ##{number}. Skipping.", type: :warn)
|
|
70
|
+
post_max_rounds_comment(pr)
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
display_message("đ Processing change request for PR ##{number} (#{pr[:title]})", type: :info)
|
|
75
|
+
|
|
76
|
+
# Fetch PR details
|
|
77
|
+
pr_data = @repository_client.fetch_pull_request(number)
|
|
78
|
+
comments = @repository_client.fetch_pr_comments(number)
|
|
79
|
+
|
|
80
|
+
# Filter comments from authorized users
|
|
81
|
+
authorized_comments = filter_authorized_comments(comments, pr_data)
|
|
82
|
+
|
|
83
|
+
if authorized_comments.empty?
|
|
84
|
+
display_message("âšī¸ No authorized comments found for PR ##{number}. Skipping.", type: :muted)
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Fetch diff to check size
|
|
89
|
+
diff = @repository_client.fetch_pull_request_diff(number)
|
|
90
|
+
diff_size = diff.lines.count
|
|
91
|
+
|
|
92
|
+
if diff_size > @config[:max_diff_size]
|
|
93
|
+
display_message("â ī¸ PR ##{number} diff too large (#{diff_size} lines > #{@config[:max_diff_size]}). Skipping.", type: :warn)
|
|
94
|
+
post_diff_too_large_comment(pr, diff_size)
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Analyze change requests
|
|
99
|
+
analysis_result = analyze_change_requests(pr_data: pr_data, comments: authorized_comments, diff: diff)
|
|
100
|
+
|
|
101
|
+
if analysis_result[:needs_clarification]
|
|
102
|
+
handle_clarification_needed(pr: pr_data, analysis: analysis_result)
|
|
103
|
+
elsif analysis_result[:can_implement]
|
|
104
|
+
implement_changes(pr: pr_data, analysis: analysis_result, diff: diff)
|
|
105
|
+
else
|
|
106
|
+
handle_cannot_implement(pr: pr_data, analysis: analysis_result)
|
|
107
|
+
end
|
|
108
|
+
rescue => e
|
|
109
|
+
display_message("â Change request processing failed: #{e.message}", type: :error)
|
|
110
|
+
Aidp.log_error("change_request_processor", "Change request failed", pr: pr[:number], error: e.message, backtrace: e.backtrace&.first(10))
|
|
111
|
+
|
|
112
|
+
# Post error comment
|
|
113
|
+
error_comment = <<~COMMENT
|
|
114
|
+
#{COMMENT_HEADER}
|
|
115
|
+
|
|
116
|
+
â Automated change request processing failed: #{e.message}
|
|
117
|
+
|
|
118
|
+
Please review the requested changes manually or retry by re-adding the `#{@change_request_label}` label.
|
|
119
|
+
COMMENT
|
|
120
|
+
begin
|
|
121
|
+
@repository_client.post_comment(pr[:number], error_comment)
|
|
122
|
+
rescue
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def filter_authorized_comments(comments, pr_data)
|
|
130
|
+
# If allowlist is empty (for private repos), consider PR author and all commenters
|
|
131
|
+
# For public repos, enforce allowlist
|
|
132
|
+
if @author_allowlist.empty?
|
|
133
|
+
# Private repo: trust all comments from PR participants
|
|
134
|
+
comments
|
|
135
|
+
else
|
|
136
|
+
# Public repo: only allow comments from allowlisted users
|
|
137
|
+
comments.select do |comment|
|
|
138
|
+
author = comment[:author]
|
|
139
|
+
@author_allowlist.include?(author)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def analyze_change_requests(pr_data:, comments:, diff:)
|
|
145
|
+
provider_name = @provider_name || detect_default_provider
|
|
146
|
+
provider = Aidp::ProviderManager.get_provider(provider_name, use_harness: false)
|
|
147
|
+
|
|
148
|
+
user_prompt = build_analysis_prompt(pr_data: pr_data, comments: comments, diff: diff)
|
|
149
|
+
full_prompt = "#{change_request_system_prompt}\n\n#{user_prompt}"
|
|
150
|
+
|
|
151
|
+
Aidp.log_debug("change_request_processor", "Analyzing change requests", pr: pr_data[:number], comments_count: comments.length)
|
|
152
|
+
|
|
153
|
+
response = provider.send_message(prompt: full_prompt)
|
|
154
|
+
content = response.to_s.strip
|
|
155
|
+
|
|
156
|
+
# Extract JSON from response
|
|
157
|
+
json_content = extract_json(content)
|
|
158
|
+
|
|
159
|
+
# Parse JSON response
|
|
160
|
+
parsed = JSON.parse(json_content)
|
|
161
|
+
|
|
162
|
+
{
|
|
163
|
+
can_implement: parsed["can_implement"],
|
|
164
|
+
needs_clarification: parsed["needs_clarification"],
|
|
165
|
+
clarifying_questions: parsed["clarifying_questions"] || [],
|
|
166
|
+
reason: parsed["reason"],
|
|
167
|
+
changes: parsed["changes"] || []
|
|
168
|
+
}
|
|
169
|
+
rescue JSON::ParserError => e
|
|
170
|
+
Aidp.log_error("change_request_processor", "Failed to parse AI response", error: e.message, content: content)
|
|
171
|
+
{can_implement: false, needs_clarification: false, reason: "Failed to parse AI analysis"}
|
|
172
|
+
rescue => e
|
|
173
|
+
Aidp.log_error("change_request_processor", "AI analysis failed", error: e.message)
|
|
174
|
+
{can_implement: false, needs_clarification: false, reason: "AI analysis error: #{e.message}"}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def change_request_system_prompt
|
|
178
|
+
<<~PROMPT
|
|
179
|
+
You are an expert software engineer analyzing change requests from PR comments.
|
|
180
|
+
|
|
181
|
+
Your task is to:
|
|
182
|
+
1. Read all comments and understand what changes are being requested
|
|
183
|
+
2. Weight newer comments higher than older ones
|
|
184
|
+
3. If multiple approved commenters request different things, consider the most recent request
|
|
185
|
+
4. Determine if you can confidently implement the requested changes
|
|
186
|
+
|
|
187
|
+
Respond in JSON format:
|
|
188
|
+
{
|
|
189
|
+
"can_implement": true/false,
|
|
190
|
+
"needs_clarification": true/false,
|
|
191
|
+
"clarifying_questions": ["Question 1?", "Question 2?"],
|
|
192
|
+
"reason": "Brief explanation of your decision",
|
|
193
|
+
"changes": [
|
|
194
|
+
{
|
|
195
|
+
"file": "path/to/file",
|
|
196
|
+
"action": "edit|create|delete",
|
|
197
|
+
"content": "Full file content after change (for create/edit)",
|
|
198
|
+
"description": "What this change does",
|
|
199
|
+
"line_start": 10,
|
|
200
|
+
"line_end": 20
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
Set "can_implement" to true ONLY if:
|
|
206
|
+
- The requested changes are clear and unambiguous
|
|
207
|
+
- You understand the codebase context from the PR diff
|
|
208
|
+
- The changes are technically feasible
|
|
209
|
+
- You can provide complete, correct implementations
|
|
210
|
+
|
|
211
|
+
Set "needs_clarification" to true if:
|
|
212
|
+
- Multiple conflicting requests exist
|
|
213
|
+
- The request is vague or incomplete
|
|
214
|
+
- You need more context to implement correctly
|
|
215
|
+
- There are unclear technical requirements
|
|
216
|
+
|
|
217
|
+
For "changes", provide the complete file content after applying the requested modifications.
|
|
218
|
+
Support multi-file edits by including multiple change objects.
|
|
219
|
+
|
|
220
|
+
DO NOT attempt to implement if:
|
|
221
|
+
- The request requires domain knowledge you don't have
|
|
222
|
+
- The changes could introduce security vulnerabilities
|
|
223
|
+
- The request is too complex for automated implementation
|
|
224
|
+
- You're not confident the changes are correct
|
|
225
|
+
PROMPT
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def build_analysis_prompt(pr_data:, comments:, diff:)
|
|
229
|
+
# Sort comments by creation time, newest first
|
|
230
|
+
sorted_comments = comments.sort_by { |c| c[:created_at] }.reverse
|
|
231
|
+
|
|
232
|
+
comments_text = sorted_comments.map do |comment|
|
|
233
|
+
"**#{comment[:author]}** (#{comment[:created_at]}):\n#{comment[:body]}"
|
|
234
|
+
end.join("\n\n---\n\n")
|
|
235
|
+
|
|
236
|
+
<<~PROMPT
|
|
237
|
+
Analyze these change requests for PR ##{pr_data[:number]}: #{pr_data[:title]}
|
|
238
|
+
|
|
239
|
+
**PR Description:**
|
|
240
|
+
#{pr_data[:body]}
|
|
241
|
+
|
|
242
|
+
**Current PR Diff:**
|
|
243
|
+
```diff
|
|
244
|
+
#{diff}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Comments (newest first):**
|
|
248
|
+
#{comments_text}
|
|
249
|
+
|
|
250
|
+
Please analyze what changes are being requested and determine if you can implement them.
|
|
251
|
+
PROMPT
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def implement_changes(pr:, analysis:, diff:)
|
|
255
|
+
display_message("đ¨ Implementing requested changes for PR ##{pr[:number]}", type: :info)
|
|
256
|
+
|
|
257
|
+
# Checkout PR branch
|
|
258
|
+
checkout_pr_branch(pr)
|
|
259
|
+
|
|
260
|
+
# Apply changes
|
|
261
|
+
apply_changes(analysis[:changes])
|
|
262
|
+
|
|
263
|
+
# Run tests if configured
|
|
264
|
+
if @config[:run_tests_before_push]
|
|
265
|
+
test_result = run_tests_and_linters
|
|
266
|
+
unless test_result[:success]
|
|
267
|
+
handle_test_failure(pr: pr, analysis: analysis, test_result: test_result)
|
|
268
|
+
return
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Commit and push
|
|
273
|
+
if commit_and_push(pr, analysis)
|
|
274
|
+
handle_success(pr: pr, analysis: analysis)
|
|
275
|
+
else
|
|
276
|
+
handle_no_changes(pr: pr, analysis: analysis)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def checkout_pr_branch(pr_data)
|
|
281
|
+
head_ref = pr_data[:head_ref]
|
|
282
|
+
|
|
283
|
+
Dir.chdir(@project_dir) do
|
|
284
|
+
# Fetch latest
|
|
285
|
+
run_git(%w[fetch origin])
|
|
286
|
+
|
|
287
|
+
# Checkout the PR branch
|
|
288
|
+
run_git(["checkout", head_ref])
|
|
289
|
+
|
|
290
|
+
# Pull latest changes
|
|
291
|
+
run_git(%w[pull --ff-only], allow_failure: true)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
Aidp.log_debug("change_request_processor", "Checked out PR branch", branch: head_ref)
|
|
295
|
+
display_message("đŋ Checked out branch: #{head_ref}", type: :info)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def apply_changes(changes)
|
|
299
|
+
changes.each do |change|
|
|
300
|
+
file_path = File.join(@project_dir, change["file"])
|
|
301
|
+
|
|
302
|
+
case change["action"]
|
|
303
|
+
when "create", "edit"
|
|
304
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
305
|
+
File.write(file_path, change["content"])
|
|
306
|
+
display_message(" â #{change["action"]} #{change["file"]}", type: :muted) if @verbose
|
|
307
|
+
Aidp.log_debug("change_request_processor", "Applied change", action: change["action"], file: change["file"])
|
|
308
|
+
when "delete"
|
|
309
|
+
File.delete(file_path) if File.exist?(file_path)
|
|
310
|
+
display_message(" â Deleted #{change["file"]}", type: :muted) if @verbose
|
|
311
|
+
Aidp.log_debug("change_request_processor", "Deleted file", file: change["file"])
|
|
312
|
+
else
|
|
313
|
+
display_message(" â ī¸ Unknown action: #{change["action"]} for #{change["file"]}", type: :warn)
|
|
314
|
+
Aidp.log_warn("change_request_processor", "Unknown change action", action: change["action"], file: change["file"])
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def run_tests_and_linters
|
|
320
|
+
display_message("đ§Ē Running tests and linters...", type: :info)
|
|
321
|
+
|
|
322
|
+
config_manager = Aidp::Harness::ConfigManager.new(@project_dir)
|
|
323
|
+
config = config_manager.config || {}
|
|
324
|
+
|
|
325
|
+
test_runner = Aidp::Harness::TestRunner.new(@project_dir, config)
|
|
326
|
+
|
|
327
|
+
# Run linters first
|
|
328
|
+
lint_result = test_runner.run_linters
|
|
329
|
+
if lint_result && !lint_result[:passed]
|
|
330
|
+
return {success: false, stage: "lint", output: lint_result[:output]}
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Run tests
|
|
334
|
+
test_result = test_runner.run_tests
|
|
335
|
+
if test_result && !test_result[:passed]
|
|
336
|
+
return {success: false, stage: "test", output: test_result[:output]}
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
{success: true}
|
|
340
|
+
rescue => e
|
|
341
|
+
Aidp.log_error("change_request_processor", "Test/lint execution failed", error: e.message)
|
|
342
|
+
{success: false, stage: "unknown", error: e.message}
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def commit_and_push(pr_data, analysis)
|
|
346
|
+
Dir.chdir(@project_dir) do
|
|
347
|
+
# Check if there are changes
|
|
348
|
+
status_output = run_git(%w[status --porcelain])
|
|
349
|
+
if status_output.strip.empty?
|
|
350
|
+
display_message("âšī¸ No changes to commit after applying changes.", type: :muted)
|
|
351
|
+
return false
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Stage all changes
|
|
355
|
+
run_git(%w[add -A])
|
|
356
|
+
|
|
357
|
+
# Create commit
|
|
358
|
+
commit_message = build_commit_message(pr_data, analysis)
|
|
359
|
+
run_git(["commit", "-m", commit_message])
|
|
360
|
+
|
|
361
|
+
display_message("đž Created commit: #{commit_message.lines.first.strip}", type: :info)
|
|
362
|
+
Aidp.log_debug("change_request_processor", "Created commit", pr: pr_data[:number])
|
|
363
|
+
|
|
364
|
+
# Push to origin
|
|
365
|
+
head_ref = pr_data[:head_ref]
|
|
366
|
+
run_git(["push", "origin", head_ref])
|
|
367
|
+
|
|
368
|
+
display_message("âŦī¸ Pushed changes to #{head_ref}", type: :success)
|
|
369
|
+
Aidp.log_info("change_request_processor", "Pushed changes", pr: pr_data[:number], branch: head_ref)
|
|
370
|
+
true
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def build_commit_message(pr_data, analysis)
|
|
375
|
+
prefix = @config[:commit_message_prefix]
|
|
376
|
+
changes_summary = analysis[:changes]&.map { |c| c["description"] }&.join(", ")
|
|
377
|
+
changes_summary = "requested changes" if changes_summary.nil? || changes_summary.empty?
|
|
378
|
+
|
|
379
|
+
message = "#{prefix}: #{changes_summary}\n\n"
|
|
380
|
+
message += "Implements change request from PR ##{pr_data[:number]} review comments.\n"
|
|
381
|
+
message += "\nReason: #{analysis[:reason]}\n" if analysis[:reason]
|
|
382
|
+
message += "\nCo-authored-by: AIDP Change Request Processor <ai@aidp.dev>"
|
|
383
|
+
|
|
384
|
+
message
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def handle_success(pr:, analysis:)
|
|
388
|
+
changes_list = analysis[:changes].map do |c|
|
|
389
|
+
"- **#{c["file"]}**: #{c["description"]}"
|
|
390
|
+
end.join("\n")
|
|
391
|
+
|
|
392
|
+
comment = <<~COMMENT
|
|
393
|
+
#{COMMENT_HEADER}
|
|
394
|
+
|
|
395
|
+
â
Successfully implemented requested changes!
|
|
396
|
+
|
|
397
|
+
**Changes Applied:**
|
|
398
|
+
#{changes_list}
|
|
399
|
+
|
|
400
|
+
The changes have been committed and pushed to this PR.
|
|
401
|
+
COMMENT
|
|
402
|
+
|
|
403
|
+
@repository_client.post_comment(pr[:number], comment)
|
|
404
|
+
@state_store.record_change_request(pr[:number], {
|
|
405
|
+
status: "completed",
|
|
406
|
+
timestamp: Time.now.utc.iso8601,
|
|
407
|
+
changes_applied: analysis[:changes].length,
|
|
408
|
+
commits: 1
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
display_message("đ Posted success comment for PR ##{pr[:number]}", type: :success)
|
|
412
|
+
Aidp.log_info("change_request_processor", "Change request completed", pr: pr[:number], changes: analysis[:changes].length)
|
|
413
|
+
|
|
414
|
+
# Remove label after successful implementation
|
|
415
|
+
begin
|
|
416
|
+
@repository_client.remove_labels(pr[:number], @change_request_label)
|
|
417
|
+
display_message("đˇī¸ Removed '#{@change_request_label}' label after successful implementation", type: :info)
|
|
418
|
+
rescue => e
|
|
419
|
+
display_message("â ī¸ Failed to remove change request label: #{e.message}", type: :warn)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def handle_no_changes(pr:, analysis:)
|
|
424
|
+
comment = <<~COMMENT
|
|
425
|
+
#{COMMENT_HEADER}
|
|
426
|
+
|
|
427
|
+
âšī¸ Analysis completed but no changes were needed.
|
|
428
|
+
|
|
429
|
+
**Reason:** #{analysis[:reason] || "The requested changes may already be applied or no modifications were necessary."}
|
|
430
|
+
|
|
431
|
+
If you believe changes should be made, please clarify the request and re-add the `#{@change_request_label}` label.
|
|
432
|
+
COMMENT
|
|
433
|
+
|
|
434
|
+
@repository_client.post_comment(pr[:number], comment)
|
|
435
|
+
@state_store.record_change_request(pr[:number], {
|
|
436
|
+
status: "no_changes",
|
|
437
|
+
timestamp: Time.now.utc.iso8601,
|
|
438
|
+
reason: analysis[:reason]
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
display_message("âšī¸ Posted no-changes comment for PR ##{pr[:number]}", type: :info)
|
|
442
|
+
|
|
443
|
+
# Remove label
|
|
444
|
+
begin
|
|
445
|
+
@repository_client.remove_labels(pr[:number], @change_request_label)
|
|
446
|
+
rescue
|
|
447
|
+
nil
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def handle_clarification_needed(pr:, analysis:)
|
|
452
|
+
existing_data = @state_store.change_request_data(pr[:number])
|
|
453
|
+
clarification_count = (existing_data&.dig("clarification_count") || 0) + 1
|
|
454
|
+
|
|
455
|
+
questions_list = analysis[:clarifying_questions].map.with_index(1) do |q, i|
|
|
456
|
+
"#{i}. #{q}"
|
|
457
|
+
end.join("\n")
|
|
458
|
+
|
|
459
|
+
comment = <<~COMMENT
|
|
460
|
+
#{COMMENT_HEADER}
|
|
461
|
+
|
|
462
|
+
đ¤ I need clarification to implement the requested changes.
|
|
463
|
+
|
|
464
|
+
**Questions:**
|
|
465
|
+
#{questions_list}
|
|
466
|
+
|
|
467
|
+
**Reason:** #{analysis[:reason]}
|
|
468
|
+
|
|
469
|
+
Please respond to these questions in a comment, then re-apply the `#{@change_request_label}` label.
|
|
470
|
+
|
|
471
|
+
_(Clarification round #{clarification_count} of #{MAX_CLARIFICATION_ROUNDS})_
|
|
472
|
+
COMMENT
|
|
473
|
+
|
|
474
|
+
@repository_client.post_comment(pr[:number], comment)
|
|
475
|
+
@state_store.record_change_request(pr[:number], {
|
|
476
|
+
status: "needs_clarification",
|
|
477
|
+
timestamp: Time.now.utc.iso8601,
|
|
478
|
+
clarification_count: clarification_count,
|
|
479
|
+
reason: analysis[:reason]
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
display_message("đ¤ Posted clarification request for PR ##{pr[:number]}", type: :info)
|
|
483
|
+
Aidp.log_info("change_request_processor", "Clarification needed", pr: pr[:number], round: clarification_count)
|
|
484
|
+
|
|
485
|
+
# Replace label with needs-input label
|
|
486
|
+
begin
|
|
487
|
+
@repository_client.replace_labels(pr[:number], old_labels: [@change_request_label], new_labels: [@needs_input_label])
|
|
488
|
+
display_message("đˇī¸ Replaced '#{@change_request_label}' with '#{@needs_input_label}' label", type: :info)
|
|
489
|
+
rescue => e
|
|
490
|
+
display_message("â ī¸ Failed to update labels: #{e.message}", type: :warn)
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def handle_cannot_implement(pr:, analysis:)
|
|
495
|
+
comment = <<~COMMENT
|
|
496
|
+
#{COMMENT_HEADER}
|
|
497
|
+
|
|
498
|
+
â ī¸ Cannot automatically implement the requested changes.
|
|
499
|
+
|
|
500
|
+
**Reason:** #{analysis[:reason] || "The request is too complex or unclear for automated implementation."}
|
|
501
|
+
|
|
502
|
+
Please consider:
|
|
503
|
+
1. Breaking down the request into smaller, more specific changes
|
|
504
|
+
2. Providing additional context or examples
|
|
505
|
+
3. Implementing the changes manually
|
|
506
|
+
|
|
507
|
+
You can retry by re-adding the `#{@change_request_label}` label with clarified instructions.
|
|
508
|
+
COMMENT
|
|
509
|
+
|
|
510
|
+
@repository_client.post_comment(pr[:number], comment)
|
|
511
|
+
@state_store.record_change_request(pr[:number], {
|
|
512
|
+
status: "cannot_implement",
|
|
513
|
+
timestamp: Time.now.utc.iso8601,
|
|
514
|
+
reason: analysis[:reason]
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
display_message("â ī¸ Posted cannot-implement comment for PR ##{pr[:number]}", type: :warn)
|
|
518
|
+
Aidp.log_info("change_request_processor", "Cannot implement", pr: pr[:number], reason: analysis[:reason])
|
|
519
|
+
|
|
520
|
+
# Remove label
|
|
521
|
+
begin
|
|
522
|
+
@repository_client.remove_labels(pr[:number], @change_request_label)
|
|
523
|
+
rescue
|
|
524
|
+
nil
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def handle_test_failure(pr:, analysis:, test_result:)
|
|
529
|
+
stage = test_result[:stage]
|
|
530
|
+
output = test_result[:output] || test_result[:error] || "Unknown error"
|
|
531
|
+
|
|
532
|
+
comment = <<~COMMENT
|
|
533
|
+
#{COMMENT_HEADER}
|
|
534
|
+
|
|
535
|
+
â Changes were applied but #{stage} failed.
|
|
536
|
+
|
|
537
|
+
**#{stage.capitalize} Output:**
|
|
538
|
+
```
|
|
539
|
+
#{output.lines.first(50).join}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Using fix-forward strategy: the changes have been committed but not pushed.
|
|
543
|
+
Please review the #{stage} failures and either:
|
|
544
|
+
1. Fix the issues manually
|
|
545
|
+
2. Provide additional context in a comment and re-add the `#{@change_request_label}` label
|
|
546
|
+
COMMENT
|
|
547
|
+
|
|
548
|
+
@repository_client.post_comment(pr[:number], comment)
|
|
549
|
+
@state_store.record_change_request(pr[:number], {
|
|
550
|
+
status: "test_failed",
|
|
551
|
+
timestamp: Time.now.utc.iso8601,
|
|
552
|
+
reason: "#{stage} failed after applying changes",
|
|
553
|
+
changes_applied: analysis[:changes].length
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
display_message("â Posted test failure comment for PR ##{pr[:number]}", type: :error)
|
|
557
|
+
Aidp.log_error("change_request_processor", "Test/lint failure", pr: pr[:number], stage: stage)
|
|
558
|
+
|
|
559
|
+
# Remove label
|
|
560
|
+
begin
|
|
561
|
+
@repository_client.remove_labels(pr[:number], @change_request_label)
|
|
562
|
+
rescue
|
|
563
|
+
nil
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def post_max_rounds_comment(pr)
|
|
568
|
+
comment = <<~COMMENT
|
|
569
|
+
#{COMMENT_HEADER}
|
|
570
|
+
|
|
571
|
+
â Maximum clarification rounds (#{MAX_CLARIFICATION_ROUNDS}) reached.
|
|
572
|
+
|
|
573
|
+
Unable to proceed with automated implementation. Please consider:
|
|
574
|
+
1. Implementing the changes manually
|
|
575
|
+
2. Creating a new, more specific change request
|
|
576
|
+
3. Providing all necessary context upfront
|
|
577
|
+
|
|
578
|
+
To reset and try again, remove the current state and re-add the `#{@change_request_label}` label.
|
|
579
|
+
COMMENT
|
|
580
|
+
|
|
581
|
+
begin
|
|
582
|
+
@repository_client.post_comment(pr[:number], comment)
|
|
583
|
+
@repository_client.remove_labels(pr[:number], @change_request_label)
|
|
584
|
+
rescue
|
|
585
|
+
nil
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def post_diff_too_large_comment(pr, diff_size)
|
|
590
|
+
comment = <<~COMMENT
|
|
591
|
+
#{COMMENT_HEADER}
|
|
592
|
+
|
|
593
|
+
â ī¸ PR diff is too large for automated change requests.
|
|
594
|
+
|
|
595
|
+
**Current size:** #{diff_size} lines
|
|
596
|
+
**Maximum allowed:** #{@config[:max_diff_size]} lines
|
|
597
|
+
|
|
598
|
+
For large PRs, please consider:
|
|
599
|
+
1. Breaking the PR into smaller chunks
|
|
600
|
+
2. Implementing changes manually
|
|
601
|
+
3. Increasing `max_diff_size` in your `aidp.yml` configuration if appropriate
|
|
602
|
+
COMMENT
|
|
603
|
+
|
|
604
|
+
begin
|
|
605
|
+
@repository_client.post_comment(pr[:number], comment)
|
|
606
|
+
@repository_client.remove_labels(pr[:number], @change_request_label)
|
|
607
|
+
rescue
|
|
608
|
+
nil
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def run_git(args, allow_failure: false)
|
|
613
|
+
stdout, stderr, status = Open3.capture3("git", *Array(args))
|
|
614
|
+
raise "git #{args.join(" ")} failed: #{stderr.strip}" unless status.success? || allow_failure
|
|
615
|
+
stdout
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def detect_default_provider
|
|
619
|
+
config_manager = Aidp::Harness::ConfigManager.new(@project_dir)
|
|
620
|
+
config_manager.default_provider || "anthropic"
|
|
621
|
+
rescue
|
|
622
|
+
"anthropic"
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def extract_json(text)
|
|
626
|
+
# Try to extract JSON from code fences or find JSON object
|
|
627
|
+
return text if text.start_with?("{") && text.end_with?("}")
|
|
628
|
+
|
|
629
|
+
# Extract from code fence
|
|
630
|
+
fence_start = text.index("```json")
|
|
631
|
+
if fence_start
|
|
632
|
+
json_start = text.index("{", fence_start)
|
|
633
|
+
fence_end = text.index("```", fence_start + 7)
|
|
634
|
+
if json_start && fence_end && json_start < fence_end
|
|
635
|
+
json_end = text.rindex("}", fence_end - 1)
|
|
636
|
+
return text[json_start..json_end] if json_end && json_end > json_start
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# Find JSON object
|
|
641
|
+
first_brace = text.index("{")
|
|
642
|
+
last_brace = text.rindex("}")
|
|
643
|
+
if first_brace && last_brace && last_brace > first_brace
|
|
644
|
+
text[first_brace..last_brace]
|
|
645
|
+
else
|
|
646
|
+
text
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def symbolize_keys(hash)
|
|
651
|
+
return {} unless hash
|
|
652
|
+
|
|
653
|
+
hash.each_with_object({}) do |(key, value), memo|
|
|
654
|
+
memo[key.to_sym] = value
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
end
|