aidp 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/lib/aidp/analyze/feature_analyzer.rb +322 -320
  4. data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
  5. data/lib/aidp/auto_update/coordinator.rb +97 -7
  6. data/lib/aidp/auto_update.rb +0 -12
  7. data/lib/aidp/cli/devcontainer_commands.rb +0 -5
  8. data/lib/aidp/cli/eval_command.rb +399 -0
  9. data/lib/aidp/cli/harness_command.rb +1 -1
  10. data/lib/aidp/cli/security_command.rb +416 -0
  11. data/lib/aidp/cli/tools_command.rb +6 -4
  12. data/lib/aidp/cli.rb +172 -4
  13. data/lib/aidp/comment_consolidator.rb +78 -0
  14. data/lib/aidp/concurrency/exec.rb +3 -0
  15. data/lib/aidp/concurrency.rb +0 -3
  16. data/lib/aidp/config.rb +113 -1
  17. data/lib/aidp/config_paths.rb +91 -0
  18. data/lib/aidp/daemon/runner.rb +8 -4
  19. data/lib/aidp/errors.rb +134 -0
  20. data/lib/aidp/evaluations/context_capture.rb +205 -0
  21. data/lib/aidp/evaluations/evaluation_record.rb +114 -0
  22. data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
  23. data/lib/aidp/evaluations.rb +23 -0
  24. data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
  25. data/lib/aidp/execute/interactive_repl.rb +6 -2
  26. data/lib/aidp/execute/prompt_evaluator.rb +359 -0
  27. data/lib/aidp/execute/repl_macros.rb +100 -1
  28. data/lib/aidp/execute/work_loop_runner.rb +719 -58
  29. data/lib/aidp/execute/work_loop_state.rb +4 -1
  30. data/lib/aidp/execute/workflow_selector.rb +3 -0
  31. data/lib/aidp/harness/ai_decision_engine.rb +79 -0
  32. data/lib/aidp/harness/ai_filter_factory.rb +285 -0
  33. data/lib/aidp/harness/capability_registry.rb +2 -0
  34. data/lib/aidp/harness/condition_detector.rb +3 -0
  35. data/lib/aidp/harness/config_loader.rb +3 -0
  36. data/lib/aidp/harness/config_schema.rb +97 -1
  37. data/lib/aidp/harness/config_validator.rb +1 -1
  38. data/lib/aidp/harness/configuration.rb +61 -5
  39. data/lib/aidp/harness/enhanced_runner.rb +14 -11
  40. data/lib/aidp/harness/error_handler.rb +3 -0
  41. data/lib/aidp/harness/filter_definition.rb +212 -0
  42. data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
  43. data/lib/aidp/harness/output_filter.rb +50 -25
  44. data/lib/aidp/harness/output_filter_config.rb +129 -0
  45. data/lib/aidp/harness/provider_factory.rb +3 -0
  46. data/lib/aidp/harness/provider_manager.rb +96 -2
  47. data/lib/aidp/harness/runner.rb +5 -12
  48. data/lib/aidp/harness/state/persistence.rb +3 -0
  49. data/lib/aidp/harness/state_manager.rb +3 -0
  50. data/lib/aidp/harness/status_display.rb +28 -20
  51. data/lib/aidp/harness/test_runner.rb +179 -41
  52. data/lib/aidp/harness/thinking_depth_manager.rb +44 -28
  53. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
  54. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
  55. data/lib/aidp/harness/ui/error_handler.rb +3 -0
  56. data/lib/aidp/harness/ui/job_monitor.rb +4 -0
  57. data/lib/aidp/harness/ui/navigation/submenu.rb +2 -2
  58. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
  59. data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
  60. data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
  61. data/lib/aidp/harness/user_interface.rb +3 -0
  62. data/lib/aidp/loader.rb +195 -0
  63. data/lib/aidp/logger.rb +3 -0
  64. data/lib/aidp/message_display.rb +31 -0
  65. data/lib/aidp/metadata/compiler.rb +29 -17
  66. data/lib/aidp/metadata/query.rb +1 -1
  67. data/lib/aidp/metadata/scanner.rb +8 -1
  68. data/lib/aidp/metadata/tool_metadata.rb +13 -13
  69. data/lib/aidp/metadata/validator.rb +10 -0
  70. data/lib/aidp/metadata.rb +16 -0
  71. data/lib/aidp/pr_worktree_manager.rb +20 -8
  72. data/lib/aidp/provider_manager.rb +4 -7
  73. data/lib/aidp/providers/base.rb +2 -0
  74. data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
  75. data/lib/aidp/security/secrets_proxy.rb +328 -0
  76. data/lib/aidp/security/secrets_registry.rb +227 -0
  77. data/lib/aidp/security/trifecta_state.rb +220 -0
  78. data/lib/aidp/security/watch_mode_handler.rb +306 -0
  79. data/lib/aidp/security/work_loop_adapter.rb +277 -0
  80. data/lib/aidp/security.rb +56 -0
  81. data/lib/aidp/setup/wizard.rb +283 -11
  82. data/lib/aidp/skills.rb +0 -5
  83. data/lib/aidp/storage/csv_storage.rb +3 -0
  84. data/lib/aidp/style_guide/selector.rb +360 -0
  85. data/lib/aidp/tooling_detector.rb +283 -16
  86. data/lib/aidp/version.rb +1 -1
  87. data/lib/aidp/watch/auto_merger.rb +274 -0
  88. data/lib/aidp/watch/auto_pr_processor.rb +125 -7
  89. data/lib/aidp/watch/build_processor.rb +16 -1
  90. data/lib/aidp/watch/change_request_processor.rb +682 -150
  91. data/lib/aidp/watch/ci_fix_processor.rb +262 -4
  92. data/lib/aidp/watch/feedback_collector.rb +191 -0
  93. data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
  94. data/lib/aidp/watch/implementation_verifier.rb +142 -1
  95. data/lib/aidp/watch/plan_generator.rb +70 -13
  96. data/lib/aidp/watch/plan_processor.rb +12 -5
  97. data/lib/aidp/watch/projects_processor.rb +286 -0
  98. data/lib/aidp/watch/repository_client.rb +871 -22
  99. data/lib/aidp/watch/review_processor.rb +33 -6
  100. data/lib/aidp/watch/runner.rb +80 -29
  101. data/lib/aidp/watch/state_store.rb +233 -0
  102. data/lib/aidp/watch/sub_issue_creator.rb +221 -0
  103. data/lib/aidp/watch.rb +5 -7
  104. data/lib/aidp/workflows/guided_agent.rb +4 -0
  105. data/lib/aidp/workstream_cleanup.rb +0 -2
  106. data/lib/aidp/workstream_executor.rb +3 -4
  107. data/lib/aidp/worktree.rb +61 -12
  108. data/lib/aidp/worktree_branch_manager.rb +347 -101
  109. data/lib/aidp.rb +21 -106
  110. data/templates/implementation/iterative_implementation.md +46 -3
  111. metadata +91 -36
  112. data/lib/aidp/config/paths.rb +0 -131
@@ -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 # Branch already exists, no base needed
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