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
|
@@ -58,6 +58,41 @@ module Aidp
|
|
|
58
58
|
|
|
59
59
|
# Fetch PR details
|
|
60
60
|
pr_data = @repository_client.fetch_pull_request(number)
|
|
61
|
+
|
|
62
|
+
# Check for merge conflicts first - attempt to resolve them
|
|
63
|
+
if pr_data[:mergeable] == false || pr_data[:merge_state_status] == "dirty"
|
|
64
|
+
display_message("⚠️ PR ##{number} has merge conflicts. Attempting to resolve...", type: :warn)
|
|
65
|
+
Aidp.log_info("ci_fix_processor", "merge_conflicts_detected_attempting_resolution",
|
|
66
|
+
pr_number: number,
|
|
67
|
+
mergeable: pr_data[:mergeable],
|
|
68
|
+
merge_state_status: pr_data[:merge_state_status])
|
|
69
|
+
|
|
70
|
+
# Attempt to resolve merge conflicts
|
|
71
|
+
merge_fix_result = resolve_merge_conflicts(pr_data: pr_data)
|
|
72
|
+
|
|
73
|
+
if merge_fix_result[:success]
|
|
74
|
+
display_message("✅ Merge conflicts resolved. Continuing to check CI status...", type: :success)
|
|
75
|
+
# Re-fetch PR data after merge resolution
|
|
76
|
+
pr_data = @repository_client.fetch_pull_request(number)
|
|
77
|
+
else
|
|
78
|
+
# Failed to resolve conflicts
|
|
79
|
+
post_merge_conflict_failure_comment(pr_data, merge_fix_result)
|
|
80
|
+
@state_store.record_ci_fix(number, {
|
|
81
|
+
status: "merge_conflicts_unresolved",
|
|
82
|
+
timestamp: Time.now.utc.iso8601,
|
|
83
|
+
reason: merge_fix_result[:reason] || "Failed to resolve merge conflicts"
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
# Remove the label so user knows it was processed
|
|
87
|
+
begin
|
|
88
|
+
@repository_client.remove_labels(number, @ci_fix_label)
|
|
89
|
+
rescue
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
61
96
|
ci_status = @repository_client.fetch_ci_status(number)
|
|
62
97
|
|
|
63
98
|
Aidp.log_debug("ci_fix_processor", "ci_status_fetched",
|
|
@@ -87,13 +122,21 @@ module Aidp
|
|
|
87
122
|
end
|
|
88
123
|
|
|
89
124
|
# Get failed checks
|
|
125
|
+
# Log all checks before filtering to help debug detection issues
|
|
126
|
+
Aidp.log_debug("ci_fix_processor", "all_checks_before_filtering",
|
|
127
|
+
pr_number: number,
|
|
128
|
+
ci_state: ci_status[:state],
|
|
129
|
+
total_checks: ci_status[:checks]&.length || 0,
|
|
130
|
+
all_checks_detailed: ci_status[:checks]&.map { |c| {name: c[:name], status: c[:status], conclusion: c[:conclusion]} })
|
|
131
|
+
|
|
90
132
|
failed_checks = ci_status[:checks].select { |check| check[:conclusion] == "failure" }
|
|
91
133
|
|
|
92
134
|
Aidp.log_debug("ci_fix_processor", "failed_checks_filtered",
|
|
93
135
|
pr_number: number,
|
|
94
136
|
total_checks: ci_status[:checks]&.length || 0,
|
|
95
137
|
failed_count: failed_checks.length,
|
|
96
|
-
failed_checks: failed_checks.map { |c| c[:name] }
|
|
138
|
+
failed_checks: failed_checks.map { |c| c[:name] },
|
|
139
|
+
non_failure_checks: ci_status[:checks]&.reject { |c| c[:conclusion] == "failure" }&.map { |c| {name: c[:name], conclusion: c[:conclusion]} })
|
|
97
140
|
|
|
98
141
|
if failed_checks.empty?
|
|
99
142
|
display_message("⚠️ No specific failed checks found for PR ##{number}.", type: :warn)
|
|
@@ -180,6 +223,84 @@ module Aidp
|
|
|
180
223
|
{success: false, error: e.message, backtrace: e.backtrace&.first(5)}
|
|
181
224
|
end
|
|
182
225
|
|
|
226
|
+
def resolve_merge_conflicts(pr_data:)
|
|
227
|
+
display_message("🔍 Analyzing merge conflicts...", type: :info)
|
|
228
|
+
|
|
229
|
+
# Setup worktree for the PR branch
|
|
230
|
+
working_dir = setup_pr_worktree(pr_data)
|
|
231
|
+
|
|
232
|
+
# Get conflicted files
|
|
233
|
+
Dir.chdir(working_dir) do
|
|
234
|
+
# Attempt merge to trigger conflict markers
|
|
235
|
+
run_git(["fetch", "origin", pr_data[:base_ref]], allow_failure: true)
|
|
236
|
+
run_git(["merge", "origin/#{pr_data[:base_ref]}"], allow_failure: true)
|
|
237
|
+
|
|
238
|
+
# Check for conflicts
|
|
239
|
+
status_output = run_git(%w[status --porcelain])
|
|
240
|
+
conflicted_files = status_output.lines
|
|
241
|
+
.select { |line| line.start_with?("UU ", "AA ", "DD ") || line.include?("both modified") }
|
|
242
|
+
.map { |line| line.split.last }
|
|
243
|
+
|
|
244
|
+
if conflicted_files.empty?
|
|
245
|
+
Aidp.log_warn("ci_fix_processor", "no_conflict_markers_found",
|
|
246
|
+
pr_number: pr_data[:number],
|
|
247
|
+
status_output: status_output)
|
|
248
|
+
return {success: false, reason: "No conflict markers found in working tree"}
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
display_message("Found #{conflicted_files.length} conflicted file(s):", type: :info)
|
|
252
|
+
conflicted_files.each { |f| display_message(" - #{f}", type: :muted) }
|
|
253
|
+
|
|
254
|
+
# Read conflict content from each file
|
|
255
|
+
conflicts = conflicted_files.map do |file|
|
|
256
|
+
if File.exist?(file)
|
|
257
|
+
content = File.read(file)
|
|
258
|
+
{
|
|
259
|
+
file: file,
|
|
260
|
+
content: content,
|
|
261
|
+
has_markers: content.include?("<<<<<<<")
|
|
262
|
+
}
|
|
263
|
+
else
|
|
264
|
+
{file: file, content: nil, has_markers: false}
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Use AI to resolve conflicts
|
|
269
|
+
resolution = analyze_conflicts_with_ai(
|
|
270
|
+
pr_data: pr_data,
|
|
271
|
+
conflicts: conflicts
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if resolution[:can_resolve]
|
|
275
|
+
# Apply resolutions
|
|
276
|
+
resolution[:resolutions].each do |res|
|
|
277
|
+
file_path = File.join(working_dir, res["file"])
|
|
278
|
+
File.write(file_path, res["resolved_content"])
|
|
279
|
+
run_git(["add", res["file"]])
|
|
280
|
+
display_message(" ✓ Resolved #{res["file"]}", type: :muted)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Complete the merge
|
|
284
|
+
commit_message = build_merge_commit_message(pr_data, resolution)
|
|
285
|
+
run_git(["commit", "-m", commit_message], allow_failure: true)
|
|
286
|
+
|
|
287
|
+
# Push the resolution
|
|
288
|
+
run_git(["push", "origin", pr_data[:head_ref]])
|
|
289
|
+
|
|
290
|
+
display_message("✅ Merge conflicts resolved and pushed", type: :success)
|
|
291
|
+
{success: true, resolution: resolution, files_resolved: conflicted_files.length}
|
|
292
|
+
else
|
|
293
|
+
{success: false, reason: resolution[:reason] || "AI could not resolve conflicts"}
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
rescue => e
|
|
297
|
+
Aidp.log_error("ci_fix_processor", "merge_conflict_resolution_failed",
|
|
298
|
+
pr_number: pr_data[:number],
|
|
299
|
+
error: e.message,
|
|
300
|
+
backtrace: e.backtrace&.first(5))
|
|
301
|
+
{success: false, error: e.message}
|
|
302
|
+
end
|
|
303
|
+
|
|
183
304
|
def analyze_failures_with_ai(pr_data:, failures:)
|
|
184
305
|
provider_name = @provider_name || detect_default_provider
|
|
185
306
|
provider = Aidp::ProviderManager.get_provider(provider_name)
|
|
@@ -210,6 +331,109 @@ module Aidp
|
|
|
210
331
|
{can_fix: false, reason: "AI analysis error: #{e.message}"}
|
|
211
332
|
end
|
|
212
333
|
|
|
334
|
+
def analyze_conflicts_with_ai(pr_data:, conflicts:)
|
|
335
|
+
provider_name = @provider_name || detect_default_provider
|
|
336
|
+
provider = Aidp::ProviderManager.get_provider(provider_name)
|
|
337
|
+
|
|
338
|
+
user_prompt = build_merge_conflict_prompt(pr_data: pr_data, conflicts: conflicts)
|
|
339
|
+
full_prompt = "#{merge_conflict_system_prompt}\n\n#{user_prompt}"
|
|
340
|
+
|
|
341
|
+
response = provider.send_message(prompt: full_prompt)
|
|
342
|
+
content = response.to_s.strip
|
|
343
|
+
|
|
344
|
+
# Extract JSON from response
|
|
345
|
+
json_content = extract_json(content)
|
|
346
|
+
|
|
347
|
+
# Parse JSON response
|
|
348
|
+
parsed = JSON.parse(json_content)
|
|
349
|
+
|
|
350
|
+
{
|
|
351
|
+
can_resolve: parsed["can_resolve"],
|
|
352
|
+
reason: parsed["reason"],
|
|
353
|
+
strategy: parsed["strategy"],
|
|
354
|
+
resolutions: parsed["resolutions"] || []
|
|
355
|
+
}
|
|
356
|
+
rescue JSON::ParserError => e
|
|
357
|
+
Aidp.log_error("ci_fix_processor", "Failed to parse merge conflict AI response",
|
|
358
|
+
error: e.message, content: content)
|
|
359
|
+
{can_resolve: false, reason: "Failed to parse AI response"}
|
|
360
|
+
rescue => e
|
|
361
|
+
Aidp.log_error("ci_fix_processor", "Merge conflict AI analysis failed", error: e.message)
|
|
362
|
+
{can_resolve: false, reason: "AI analysis error: #{e.message}"}
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def merge_conflict_system_prompt
|
|
366
|
+
<<~PROMPT
|
|
367
|
+
You are an expert at resolving Git merge conflicts. Your task is to analyze conflicted files and produce clean, resolved versions.
|
|
368
|
+
|
|
369
|
+
When analyzing merge conflicts:
|
|
370
|
+
1. Understand the intent of both the current changes (HEAD) and incoming changes (base branch)
|
|
371
|
+
2. Look for semantic conflicts beyond just text differences
|
|
372
|
+
3. Prefer to keep both changes when they serve different purposes
|
|
373
|
+
4. Remove duplicate code or conflicting implementations
|
|
374
|
+
5. Maintain code style and conventions from the existing codebase
|
|
375
|
+
|
|
376
|
+
Respond in JSON format:
|
|
377
|
+
{
|
|
378
|
+
"can_resolve": true/false,
|
|
379
|
+
"reason": "Why you can or cannot resolve these conflicts",
|
|
380
|
+
"strategy": "Brief description of your resolution strategy",
|
|
381
|
+
"resolutions": [
|
|
382
|
+
{
|
|
383
|
+
"file": "path/to/file",
|
|
384
|
+
"resolved_content": "Complete file content with conflicts resolved",
|
|
385
|
+
"description": "What changes were made to resolve the conflict"
|
|
386
|
+
}
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
ONLY set can_resolve to true if you are confident the resolution:
|
|
391
|
+
- Maintains the intent of both branches
|
|
392
|
+
- Doesn't introduce bugs or break functionality
|
|
393
|
+
- Follows the project's coding style
|
|
394
|
+
- Compiles/parses correctly
|
|
395
|
+
|
|
396
|
+
DO NOT attempt to resolve if:
|
|
397
|
+
- The conflicts involve complex business logic you don't understand
|
|
398
|
+
- The changes are fundamentally incompatible
|
|
399
|
+
- The resolution would require significant refactoring
|
|
400
|
+
- There's insufficient context to make a safe decision
|
|
401
|
+
PROMPT
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def build_merge_conflict_prompt(pr_data:, conflicts:)
|
|
405
|
+
conflict_details = conflicts.map do |c|
|
|
406
|
+
if c[:content] && c[:has_markers]
|
|
407
|
+
"File: #{c[:file]}\n```\n#{c[:content]}\n```"
|
|
408
|
+
else
|
|
409
|
+
"File: #{c[:file]}\n(No conflict markers found or file doesn't exist)"
|
|
410
|
+
end
|
|
411
|
+
end.join("\n\n")
|
|
412
|
+
|
|
413
|
+
<<~PROMPT
|
|
414
|
+
Resolve merge conflicts for PR ##{pr_data[:number]}: #{pr_data[:title]}
|
|
415
|
+
|
|
416
|
+
Base branch: #{pr_data[:base_ref]}
|
|
417
|
+
PR branch: #{pr_data[:head_ref]}
|
|
418
|
+
|
|
419
|
+
Conflicted files:
|
|
420
|
+
#{conflict_details}
|
|
421
|
+
|
|
422
|
+
Analyze the conflicts and provide resolved versions of each file.
|
|
423
|
+
PROMPT
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def build_merge_commit_message(pr_data, resolution)
|
|
427
|
+
<<~MESSAGE.strip
|
|
428
|
+
aidp: resolve merge conflicts for PR ##{pr_data[:number]}
|
|
429
|
+
|
|
430
|
+
#{resolution[:strategy] || "Automatically resolved merge conflicts"}
|
|
431
|
+
|
|
432
|
+
Files resolved:
|
|
433
|
+
#{resolution[:resolutions].map { |r| "- #{r["file"]}: #{r["description"]}" }.join("\n")}
|
|
434
|
+
MESSAGE
|
|
435
|
+
end
|
|
436
|
+
|
|
213
437
|
def ci_fix_system_prompt
|
|
214
438
|
<<~PROMPT
|
|
215
439
|
You are an expert CI/CD troubleshooter. Your task is to analyze CI failures and propose fixes.
|
|
@@ -324,20 +548,31 @@ module Aidp
|
|
|
324
548
|
slug = "pr-#{pr_number}-ci-fix"
|
|
325
549
|
display_message("🌿 Creating worktree for PR ##{pr_number}: #{head_ref}", type: :info)
|
|
326
550
|
|
|
327
|
-
# Fetch the branch first
|
|
551
|
+
# Fetch the branch first to ensure we have the latest refs
|
|
328
552
|
Dir.chdir(@project_dir) do
|
|
329
553
|
run_git(%w[fetch origin])
|
|
330
554
|
end
|
|
331
555
|
|
|
332
|
-
# Create worktree
|
|
556
|
+
# Create worktree - Worktree.create will automatically use origin/head_ref
|
|
557
|
+
# as base if the branch only exists on the remote (e.g., PRs from Claude Code Web)
|
|
333
558
|
result = Aidp::Worktree.create(
|
|
334
559
|
slug: slug,
|
|
335
560
|
project_dir: @project_dir,
|
|
336
561
|
branch: head_ref,
|
|
337
|
-
base_branch: nil
|
|
562
|
+
base_branch: nil
|
|
338
563
|
)
|
|
339
564
|
|
|
340
565
|
worktree_path = result[:path]
|
|
566
|
+
|
|
567
|
+
# Ensure the local branch tracks the remote and has latest changes
|
|
568
|
+
# This handles cases where the branch was created from origin/branch
|
|
569
|
+
Dir.chdir(worktree_path) do
|
|
570
|
+
# Set upstream tracking if not already set
|
|
571
|
+
run_git(["branch", "--set-upstream-to=origin/#{head_ref}", head_ref], allow_failure: true)
|
|
572
|
+
# Pull any changes that may have been pushed since fetch
|
|
573
|
+
run_git(%w[pull --ff-only], allow_failure: true)
|
|
574
|
+
end
|
|
575
|
+
|
|
341
576
|
Aidp.log_debug("ci_fix_processor", "worktree_created", pr_number: pr_number, branch: head_ref, path: worktree_path)
|
|
342
577
|
display_message("✅ Worktree created at #{worktree_path}", type: :success)
|
|
343
578
|
|
|
@@ -484,6 +719,29 @@ module Aidp
|
|
|
484
719
|
@repository_client.post_comment(pr_data[:number], comment)
|
|
485
720
|
end
|
|
486
721
|
|
|
722
|
+
def post_merge_conflict_failure_comment(pr_data, merge_fix_result)
|
|
723
|
+
reason = merge_fix_result[:reason] || merge_fix_result[:error] || "Unknown error"
|
|
724
|
+
|
|
725
|
+
comment = <<~COMMENT
|
|
726
|
+
#{COMMENT_HEADER}
|
|
727
|
+
|
|
728
|
+
⚠️ This PR has merge conflicts that could not be automatically resolved.
|
|
729
|
+
|
|
730
|
+
**Reason:** #{reason}
|
|
731
|
+
|
|
732
|
+
**Next Steps:**
|
|
733
|
+
1. Manually resolve the merge conflicts in this PR
|
|
734
|
+
2. Run `git merge origin/#{pr_data[:base_ref]}` locally to see conflicts
|
|
735
|
+
3. Resolve conflicts in each file
|
|
736
|
+
4. Commit and push the resolved changes
|
|
737
|
+
5. Re-add the `#{@ci_fix_label}` label to retry automated fixes
|
|
738
|
+
|
|
739
|
+
**Tip:** Use `git status` to see which files have conflicts, and look for conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) in those files.
|
|
740
|
+
COMMENT
|
|
741
|
+
|
|
742
|
+
@repository_client.post_comment(pr_data[:number], comment)
|
|
743
|
+
end
|
|
744
|
+
|
|
487
745
|
def run_git(args, allow_failure: false)
|
|
488
746
|
stdout, stderr, status = Open3.capture3("git", *Array(args))
|
|
489
747
|
raise "git #{args.join(" ")} failed: #{stderr.strip}" unless status.success? || allow_failure
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../evaluations"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Watch
|
|
7
|
+
# Collects user feedback via GitHub reactions and converts to evaluations
|
|
8
|
+
#
|
|
9
|
+
# Monitors reactions on AIDP-posted comments:
|
|
10
|
+
# - 👍 (+1) = good rating
|
|
11
|
+
# - 👎 (-1) = bad rating
|
|
12
|
+
# - 😕 (confused) = neutral rating
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# collector = FeedbackCollector.new(
|
|
16
|
+
# repository_client: client,
|
|
17
|
+
# state_store: store,
|
|
18
|
+
# project_dir: Dir.pwd
|
|
19
|
+
# )
|
|
20
|
+
# collector.collect_feedback
|
|
21
|
+
class FeedbackCollector
|
|
22
|
+
# Mapping from GitHub reaction content to evaluation rating
|
|
23
|
+
REACTION_RATINGS = {
|
|
24
|
+
"+1" => "good",
|
|
25
|
+
"-1" => "bad",
|
|
26
|
+
"confused" => "neutral",
|
|
27
|
+
"heart" => "good",
|
|
28
|
+
"hooray" => "good",
|
|
29
|
+
"rocket" => "good",
|
|
30
|
+
"eyes" => "neutral"
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# Feedback prompt text to include in comments
|
|
34
|
+
FEEDBACK_PROMPT = <<~PROMPT.strip
|
|
35
|
+
---
|
|
36
|
+
**Rate this output**: React with 👍 (good), 👎 (bad), or 😕 (neutral) to help improve AIDP.
|
|
37
|
+
PROMPT
|
|
38
|
+
|
|
39
|
+
def initialize(repository_client:, state_store:, project_dir: Dir.pwd)
|
|
40
|
+
@repository_client = repository_client
|
|
41
|
+
@state_store = state_store
|
|
42
|
+
@project_dir = project_dir
|
|
43
|
+
@evaluation_storage = Evaluations::EvaluationStorage.new(project_dir: project_dir)
|
|
44
|
+
|
|
45
|
+
Aidp.log_debug("feedback_collector", "initialize",
|
|
46
|
+
repo: repository_client.full_repo, project_dir: project_dir)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Collect feedback from all tracked comments
|
|
50
|
+
#
|
|
51
|
+
# @return [Array<Hash>] List of new evaluations recorded
|
|
52
|
+
def collect_feedback
|
|
53
|
+
Aidp.log_debug("feedback_collector", "collect_feedback_start")
|
|
54
|
+
|
|
55
|
+
tracked_comments = @state_store.tracked_comments
|
|
56
|
+
return [] if tracked_comments.empty?
|
|
57
|
+
|
|
58
|
+
new_evaluations = []
|
|
59
|
+
|
|
60
|
+
tracked_comments.each do |comment_info|
|
|
61
|
+
evaluations = process_comment_reactions(comment_info)
|
|
62
|
+
new_evaluations.concat(evaluations)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
Aidp.log_debug("feedback_collector", "collect_feedback_complete",
|
|
66
|
+
tracked_count: tracked_comments.size, new_evaluations: new_evaluations.size)
|
|
67
|
+
|
|
68
|
+
new_evaluations
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Process reactions on a specific comment and create evaluations
|
|
72
|
+
#
|
|
73
|
+
# @param comment_info [Hash] Comment tracking info from state store
|
|
74
|
+
# @return [Array<Hash>] New evaluations created
|
|
75
|
+
def process_comment_reactions(comment_info)
|
|
76
|
+
comment_id = comment_info[:comment_id] || comment_info["comment_id"]
|
|
77
|
+
return [] unless comment_id
|
|
78
|
+
|
|
79
|
+
processor_type = comment_info[:processor_type] || comment_info["processor_type"]
|
|
80
|
+
target_number = comment_info[:number] || comment_info["number"]
|
|
81
|
+
|
|
82
|
+
Aidp.log_debug("feedback_collector", "process_comment",
|
|
83
|
+
comment_id: comment_id, processor_type: processor_type, number: target_number)
|
|
84
|
+
|
|
85
|
+
# Fetch reactions from GitHub
|
|
86
|
+
reactions = @repository_client.fetch_comment_reactions(comment_id)
|
|
87
|
+
return [] if reactions.empty?
|
|
88
|
+
|
|
89
|
+
# Get already-processed reaction IDs
|
|
90
|
+
processed_ids = @state_store.processed_reaction_ids(comment_id)
|
|
91
|
+
|
|
92
|
+
new_evaluations = []
|
|
93
|
+
|
|
94
|
+
reactions.each do |reaction|
|
|
95
|
+
reaction_id = reaction[:id]
|
|
96
|
+
next if processed_ids.include?(reaction_id)
|
|
97
|
+
|
|
98
|
+
rating = reaction_to_rating(reaction[:content])
|
|
99
|
+
next unless rating
|
|
100
|
+
|
|
101
|
+
# Create evaluation record
|
|
102
|
+
evaluation = create_evaluation(
|
|
103
|
+
rating: rating,
|
|
104
|
+
processor_type: processor_type,
|
|
105
|
+
target_number: target_number,
|
|
106
|
+
reaction: reaction
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if evaluation
|
|
110
|
+
new_evaluations << evaluation
|
|
111
|
+
@state_store.mark_reaction_processed(comment_id, reaction_id)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
new_evaluations
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Convert GitHub reaction content to evaluation rating
|
|
119
|
+
#
|
|
120
|
+
# @param content [String] GitHub reaction content (e.g., "+1", "-1", "confused")
|
|
121
|
+
# @return [String, nil] Rating or nil if not mappable
|
|
122
|
+
def reaction_to_rating(content)
|
|
123
|
+
REACTION_RATINGS[content]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Append feedback prompt to a comment body
|
|
127
|
+
#
|
|
128
|
+
# @param body [String] Original comment body
|
|
129
|
+
# @return [String] Comment body with feedback prompt
|
|
130
|
+
def self.append_feedback_prompt(body)
|
|
131
|
+
"#{body}\n\n#{FEEDBACK_PROMPT}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def create_evaluation(rating:, processor_type:, target_number:, reaction:)
|
|
137
|
+
repo = @repository_client.full_repo
|
|
138
|
+
|
|
139
|
+
context = {
|
|
140
|
+
watch: {
|
|
141
|
+
repo: repo,
|
|
142
|
+
number: target_number,
|
|
143
|
+
processor_type: processor_type
|
|
144
|
+
},
|
|
145
|
+
feedback_source: "github_reaction",
|
|
146
|
+
reaction: {
|
|
147
|
+
content: reaction[:content],
|
|
148
|
+
user: reaction[:user],
|
|
149
|
+
created_at: reaction[:created_at]
|
|
150
|
+
},
|
|
151
|
+
environment: {
|
|
152
|
+
aidp_version: defined?(Aidp::VERSION) ? Aidp::VERSION : nil
|
|
153
|
+
},
|
|
154
|
+
timestamp: Time.now.iso8601
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
record = Evaluations::EvaluationRecord.new(
|
|
158
|
+
rating: rating,
|
|
159
|
+
comment: "Feedback via GitHub reaction (#{reaction[:content]}) by #{reaction[:user]}",
|
|
160
|
+
target_type: processor_type,
|
|
161
|
+
target_id: "#{repo}##{target_number}",
|
|
162
|
+
context: context
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
result = @evaluation_storage.store(record)
|
|
166
|
+
|
|
167
|
+
if result[:success]
|
|
168
|
+
Aidp.log_info("feedback_collector", "evaluation_recorded",
|
|
169
|
+
id: record.id, rating: rating, user: reaction[:user],
|
|
170
|
+
processor_type: processor_type, target: "#{repo}##{target_number}")
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
id: record.id,
|
|
174
|
+
rating: rating,
|
|
175
|
+
user: reaction[:user],
|
|
176
|
+
processor_type: processor_type,
|
|
177
|
+
target: "#{repo}##{target_number}"
|
|
178
|
+
}
|
|
179
|
+
else
|
|
180
|
+
Aidp.log_error("feedback_collector", "evaluation_store_failed",
|
|
181
|
+
error: result[:error], reaction_id: reaction[:id])
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
184
|
+
rescue => e
|
|
185
|
+
Aidp.log_error("feedback_collector", "create_evaluation_failed",
|
|
186
|
+
error: e.message, reaction: reaction)
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|