aidp 0.24.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +72 -7
  3. data/lib/aidp/analyze/error_handler.rb +11 -0
  4. data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
  5. data/lib/aidp/auto_update/checkpoint.rb +178 -0
  6. data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
  7. data/lib/aidp/auto_update/coordinator.rb +204 -0
  8. data/lib/aidp/auto_update/errors.rb +17 -0
  9. data/lib/aidp/auto_update/failure_tracker.rb +162 -0
  10. data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
  11. data/lib/aidp/auto_update/update_check.rb +106 -0
  12. data/lib/aidp/auto_update/update_logger.rb +143 -0
  13. data/lib/aidp/auto_update/update_policy.rb +109 -0
  14. data/lib/aidp/auto_update/version_detector.rb +144 -0
  15. data/lib/aidp/auto_update.rb +52 -0
  16. data/lib/aidp/cli.rb +165 -1
  17. data/lib/aidp/execute/work_loop_runner.rb +225 -55
  18. data/lib/aidp/harness/config_loader.rb +20 -11
  19. data/lib/aidp/harness/config_schema.rb +80 -8
  20. data/lib/aidp/harness/configuration.rb +73 -2
  21. data/lib/aidp/harness/filter_strategy.rb +45 -0
  22. data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
  23. data/lib/aidp/harness/output_filter.rb +136 -0
  24. data/lib/aidp/harness/provider_factory.rb +2 -0
  25. data/lib/aidp/harness/provider_manager.rb +18 -3
  26. data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
  27. data/lib/aidp/harness/test_runner.rb +165 -27
  28. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
  29. data/lib/aidp/logger.rb +35 -5
  30. data/lib/aidp/message_display.rb +56 -2
  31. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
  32. data/lib/aidp/provider_manager.rb +2 -0
  33. data/lib/aidp/providers/kilocode.rb +202 -0
  34. data/lib/aidp/safe_directory.rb +10 -3
  35. data/lib/aidp/setup/provider_registry.rb +15 -0
  36. data/lib/aidp/setup/wizard.rb +12 -4
  37. data/lib/aidp/skills/composer.rb +4 -0
  38. data/lib/aidp/skills/loader.rb +3 -1
  39. data/lib/aidp/storage/csv_storage.rb +9 -3
  40. data/lib/aidp/storage/file_manager.rb +8 -2
  41. data/lib/aidp/storage/json_storage.rb +9 -3
  42. data/lib/aidp/version.rb +1 -1
  43. data/lib/aidp/watch/build_processor.rb +106 -17
  44. data/lib/aidp/watch/change_request_processor.rb +659 -0
  45. data/lib/aidp/watch/ci_fix_processor.rb +448 -0
  46. data/lib/aidp/watch/plan_processor.rb +81 -8
  47. data/lib/aidp/watch/repository_client.rb +465 -20
  48. data/lib/aidp/watch/review_processor.rb +266 -0
  49. data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
  50. data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
  51. data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
  52. data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
  53. data/lib/aidp/watch/runner.rb +222 -0
  54. data/lib/aidp/watch/state_store.rb +99 -1
  55. data/lib/aidp/workstream_executor.rb +5 -2
  56. data/lib/aidp.rb +5 -0
  57. data/templates/aidp.yml.example +53 -0
  58. metadata +25 -1
@@ -61,6 +61,14 @@ module Aidp
61
61
  gh_available? ? post_comment_via_gh(number, body) : post_comment_via_api(number, body)
62
62
  end
63
63
 
64
+ def find_comment(number, header_text)
65
+ gh_available? ? find_comment_via_gh(number, header_text) : find_comment_via_api(number, header_text)
66
+ end
67
+
68
+ def update_comment(comment_id, body)
69
+ gh_available? ? update_comment_via_gh(comment_id, body) : update_comment_via_api(comment_id, body)
70
+ end
71
+
64
72
  def create_pull_request(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil)
65
73
  gh_available? ? create_pull_request_via_gh(title: title, body: body, head: head, base: base, issue_number: issue_number, draft: draft, assignee: assignee) : raise("GitHub CLI not available - cannot create PR")
66
74
  end
@@ -79,6 +87,39 @@ module Aidp
79
87
  add_labels(number, *new_labels) unless new_labels.empty?
80
88
  end
81
89
 
90
+ def most_recent_label_actor(number)
91
+ gh_available? ? most_recent_label_actor_via_gh(number) : nil
92
+ end
93
+
94
+ # PR-specific operations
95
+ def fetch_pull_request(number)
96
+ gh_available? ? fetch_pull_request_via_gh(number) : fetch_pull_request_via_api(number)
97
+ end
98
+
99
+ def fetch_pull_request_diff(number)
100
+ gh_available? ? fetch_pull_request_diff_via_gh(number) : fetch_pull_request_diff_via_api(number)
101
+ end
102
+
103
+ def fetch_pull_request_files(number)
104
+ gh_available? ? fetch_pull_request_files_via_gh(number) : fetch_pull_request_files_via_api(number)
105
+ end
106
+
107
+ def fetch_ci_status(number)
108
+ gh_available? ? fetch_ci_status_via_gh(number) : fetch_ci_status_via_api(number)
109
+ end
110
+
111
+ def post_review_comment(number, body, commit_id: nil, path: nil, line: nil)
112
+ gh_available? ? post_review_comment_via_gh(number, body, commit_id: commit_id, path: path, line: line) : post_review_comment_via_api(number, body, commit_id: commit_id, path: path, line: line)
113
+ end
114
+
115
+ def list_pull_requests(labels: [], state: "open")
116
+ gh_available? ? list_pull_requests_via_gh(labels: labels, state: state) : list_pull_requests_via_api(labels: labels, state: state)
117
+ end
118
+
119
+ def fetch_pr_comments(number)
120
+ gh_available? ? fetch_pr_comments_via_gh(number) : fetch_pr_comments_via_api(number)
121
+ end
122
+
82
123
  private
83
124
 
84
125
  def list_issues_via_gh(labels:, state:)
@@ -133,27 +174,11 @@ module Aidp
133
174
  raise "GitHub API error (#{response.code})" unless response.code == "200"
134
175
 
135
176
  data = JSON.parse(response.body)
136
- comments = fetch_comments_via_api(number)
177
+ comments = fetch_pr_comments_via_api(number)
137
178
  data["comments"] = comments
138
179
  normalize_issue_detail_api(data)
139
180
  end
140
181
 
141
- def fetch_comments_via_api(number)
142
- uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/comments")
143
- response = Net::HTTP.get_response(uri)
144
- return [] unless response.code == "200"
145
-
146
- JSON.parse(response.body).map do |raw|
147
- {
148
- "body" => raw["body"],
149
- "author" => raw.dig("user", "login"),
150
- "createdAt" => raw["created_at"]
151
- }
152
- end
153
- rescue
154
- []
155
- end
156
-
157
182
  def post_comment_via_gh(number, body)
158
183
  cmd = ["gh", "issue", "comment", number.to_s, "--repo", full_repo, "--body", body]
159
184
  stdout, stderr, status = Open3.capture3(*cmd)
@@ -176,6 +201,44 @@ module Aidp
176
201
  response.body
177
202
  end
178
203
 
204
+ def find_comment_via_gh(number, header_text)
205
+ comments = fetch_pr_comments_via_gh(number)
206
+ comments.find { |comment| comment[:body]&.include?(header_text) }
207
+ rescue => e
208
+ Aidp.log_warn("repository_client", "Failed to find comment", error: e.message)
209
+ nil
210
+ end
211
+
212
+ def find_comment_via_api(number, header_text)
213
+ comments = fetch_pr_comments_via_api(number)
214
+ comments.find { |comment| comment[:body]&.include?(header_text) }
215
+ rescue => e
216
+ Aidp.log_warn("repository_client", "Failed to find comment", error: e.message)
217
+ nil
218
+ end
219
+
220
+ def update_comment_via_gh(comment_id, body)
221
+ cmd = ["gh", "api", "repos/#{full_repo}/issues/comments/#{comment_id}", "-X", "PATCH", "-f", "body=#{body}"]
222
+ stdout, stderr, status = Open3.capture3(*cmd)
223
+ raise "Failed to update comment via gh: #{stderr.strip}" unless status.success?
224
+
225
+ stdout.strip
226
+ end
227
+
228
+ def update_comment_via_api(comment_id, body)
229
+ uri = URI("https://api.github.com/repos/#{full_repo}/issues/comments/#{comment_id}")
230
+ request = Net::HTTP::Patch.new(uri)
231
+ request["Content-Type"] = "application/json"
232
+ request.body = JSON.dump({body: body})
233
+
234
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
235
+ http.request(request)
236
+ end
237
+
238
+ raise "GitHub API update comment failed (#{response.code})" unless response.code.start_with?("2")
239
+ response.body
240
+ end
241
+
179
242
  def create_pull_request_via_gh(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil)
180
243
  cmd = [
181
244
  "gh", "pr", "create",
@@ -254,6 +317,378 @@ module Aidp
254
317
  end
255
318
  end
256
319
 
320
+ def most_recent_label_actor_via_gh(number)
321
+ # Use GitHub GraphQL API via gh cli to fetch the most recent label event actor
322
+ query = <<~GRAPHQL
323
+ query($owner: String!, $repo: String!, $number: Int!) {
324
+ repository(owner: $owner, name: $repo) {
325
+ issue(number: $number) {
326
+ timelineItems(last: 100, itemTypes: [LABELED_EVENT]) {
327
+ nodes {
328
+ ... on LabeledEvent {
329
+ createdAt
330
+ actor {
331
+ login
332
+ }
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+ }
339
+ GRAPHQL
340
+
341
+ cmd = [
342
+ "gh", "api", "graphql",
343
+ "-f", "query=#{query}",
344
+ "-F", "owner=#{owner}",
345
+ "-F", "repo=#{repo}",
346
+ "-F", "number=#{number}"
347
+ ]
348
+
349
+ stdout, stderr, status = Open3.capture3(*cmd)
350
+ unless status.success?
351
+ Aidp.log_warn("repository_client", "Failed to fetch label events via GraphQL", error: stderr.strip)
352
+ return nil
353
+ end
354
+
355
+ data = JSON.parse(stdout)
356
+ events = data.dig("data", "repository", "issue", "timelineItems", "nodes") || []
357
+
358
+ # Filter out events without actors and sort by createdAt to get most recent
359
+ valid_events = events.select { |event| event.dig("actor", "login") }
360
+ return nil if valid_events.empty?
361
+
362
+ most_recent = valid_events.max_by { |event| event["createdAt"] }
363
+ most_recent.dig("actor", "login")
364
+ rescue JSON::ParserError => e
365
+ Aidp.log_warn("repository_client", "Failed to parse GraphQL response", error: e.message)
366
+ nil
367
+ rescue => e
368
+ Aidp.log_warn("repository_client", "Unexpected error fetching label actor", error: e.message)
369
+ nil
370
+ end
371
+
372
+ # PR operations via gh CLI
373
+ def list_pull_requests_via_gh(labels:, state:)
374
+ json_fields = %w[number title labels updatedAt state url headRefName baseRefName]
375
+ cmd = ["gh", "pr", "list", "--repo", full_repo, "--state", state, "--json", json_fields.join(",")]
376
+ labels.each do |label|
377
+ cmd += ["--label", label]
378
+ end
379
+
380
+ stdout, stderr, status = Open3.capture3(*cmd)
381
+ unless status.success?
382
+ warn("GitHub CLI PR list failed: #{stderr}")
383
+ return []
384
+ end
385
+
386
+ JSON.parse(stdout).map { |raw| normalize_pull_request(raw) }
387
+ rescue JSON::ParserError => e
388
+ warn("Failed to parse GH CLI PR list response: #{e.message}")
389
+ []
390
+ end
391
+
392
+ def list_pull_requests_via_api(labels:, state:)
393
+ label_param = labels.join(",")
394
+ uri = URI("https://api.github.com/repos/#{full_repo}/pulls?state=#{state}")
395
+ uri.query = [uri.query, "labels=#{URI.encode_www_form_component(label_param)}"].compact.join("&") unless label_param.empty?
396
+
397
+ response = Net::HTTP.get_response(uri)
398
+ return [] unless response.code == "200"
399
+
400
+ JSON.parse(response.body).map { |raw| normalize_pull_request_api(raw) }
401
+ rescue => e
402
+ warn("GitHub API PR list failed: #{e.message}")
403
+ []
404
+ end
405
+
406
+ def fetch_pull_request_via_gh(number)
407
+ fields = %w[number title body labels state url headRefName baseRefName commits author mergeable]
408
+ cmd = ["gh", "pr", "view", number.to_s, "--repo", full_repo, "--json", fields.join(",")]
409
+
410
+ stdout, stderr, status = Open3.capture3(*cmd)
411
+ raise "GitHub CLI error: #{stderr.strip}" unless status.success?
412
+
413
+ data = JSON.parse(stdout)
414
+ normalize_pull_request_detail(data)
415
+ rescue JSON::ParserError => e
416
+ raise "Failed to parse GitHub CLI PR response: #{e.message}"
417
+ end
418
+
419
+ def fetch_pull_request_via_api(number)
420
+ uri = URI("https://api.github.com/repos/#{full_repo}/pulls/#{number}")
421
+ response = Net::HTTP.get_response(uri)
422
+ raise "GitHub API error (#{response.code})" unless response.code == "200"
423
+
424
+ data = JSON.parse(response.body)
425
+ normalize_pull_request_detail_api(data)
426
+ end
427
+
428
+ def fetch_pull_request_diff_via_gh(number)
429
+ cmd = ["gh", "pr", "diff", number.to_s, "--repo", full_repo]
430
+ stdout, stderr, status = Open3.capture3(*cmd)
431
+ raise "Failed to fetch PR diff via gh: #{stderr.strip}" unless status.success?
432
+
433
+ stdout
434
+ end
435
+
436
+ def fetch_pull_request_diff_via_api(number)
437
+ uri = URI("https://api.github.com/repos/#{full_repo}/pulls/#{number}")
438
+ request = Net::HTTP::Get.new(uri)
439
+ request["Accept"] = "application/vnd.github.v3.diff"
440
+
441
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
442
+ http.request(request)
443
+ end
444
+
445
+ raise "GitHub API diff failed (#{response.code})" unless response.code == "200"
446
+ response.body
447
+ end
448
+
449
+ def fetch_pull_request_files_via_gh(number)
450
+ # Use gh api to fetch changed files
451
+ cmd = ["gh", "api", "repos/#{full_repo}/pulls/#{number}/files", "--jq", "."]
452
+ stdout, stderr, status = Open3.capture3(*cmd)
453
+ raise "Failed to fetch PR files via gh: #{stderr.strip}" unless status.success?
454
+
455
+ JSON.parse(stdout).map { |file| normalize_pr_file(file) }
456
+ rescue JSON::ParserError => e
457
+ raise "Failed to parse PR files response: #{e.message}"
458
+ end
459
+
460
+ def fetch_pull_request_files_via_api(number)
461
+ uri = URI("https://api.github.com/repos/#{full_repo}/pulls/#{number}/files")
462
+ response = Net::HTTP.get_response(uri)
463
+ raise "GitHub API files failed (#{response.code})" unless response.code == "200"
464
+
465
+ JSON.parse(response.body).map { |file| normalize_pr_file(file) }
466
+ rescue JSON::ParserError => e
467
+ raise "Failed to parse PR files response: #{e.message}"
468
+ end
469
+
470
+ def fetch_ci_status_via_gh(number)
471
+ # Fetch PR to get the head SHA
472
+ pr_data = fetch_pull_request_via_gh(number)
473
+ head_sha = pr_data[:head_sha]
474
+
475
+ # Fetch check runs for the commit
476
+ cmd = ["gh", "api", "repos/#{full_repo}/commits/#{head_sha}/check-runs", "--jq", "."]
477
+ stdout, _stderr, status = Open3.capture3(*cmd)
478
+
479
+ if status.success?
480
+ data = JSON.parse(stdout)
481
+ check_runs = data["check_runs"] || []
482
+ normalize_ci_status(check_runs, head_sha)
483
+ else
484
+ # Fallback to status checks
485
+ {sha: head_sha, state: "unknown", checks: []}
486
+ end
487
+ rescue => e
488
+ Aidp.log_warn("repository_client", "Failed to fetch CI status", error: e.message)
489
+ {sha: nil, state: "unknown", checks: []}
490
+ end
491
+
492
+ def fetch_ci_status_via_api(number)
493
+ pr_data = fetch_pull_request_via_api(number)
494
+ head_sha = pr_data[:head_sha]
495
+
496
+ uri = URI("https://api.github.com/repos/#{full_repo}/commits/#{head_sha}/check-runs")
497
+ request = Net::HTTP::Get.new(uri)
498
+ request["Accept"] = "application/vnd.github.v3+json"
499
+
500
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
501
+ http.request(request)
502
+ end
503
+
504
+ if response.code == "200"
505
+ data = JSON.parse(response.body)
506
+ check_runs = data["check_runs"] || []
507
+ normalize_ci_status(check_runs, head_sha)
508
+ else
509
+ {sha: head_sha, state: "unknown", checks: []}
510
+ end
511
+ rescue => e
512
+ Aidp.log_warn("repository_client", "Failed to fetch CI status", error: e.message)
513
+ {sha: nil, state: "unknown", checks: []}
514
+ end
515
+
516
+ def post_review_comment_via_gh(number, body, commit_id: nil, path: nil, line: nil)
517
+ if path && line && commit_id
518
+ # Note: gh CLI doesn't support inline comments directly, so we use the API
519
+ # For inline comments, we need to use the GitHub API
520
+ post_review_comment_via_api(number, body, commit_id: commit_id, path: path, line: line)
521
+ else
522
+ # Post general review comment
523
+ cmd = ["gh", "pr", "comment", number.to_s, "--repo", full_repo, "--body", body]
524
+ stdout, stderr, status = Open3.capture3(*cmd)
525
+ raise "Failed to post review comment via gh: #{stderr.strip}" unless status.success?
526
+
527
+ stdout.strip
528
+ end
529
+ end
530
+
531
+ def post_review_comment_via_api(number, body, commit_id: nil, path: nil, line: nil)
532
+ uri, request = if path && line && commit_id
533
+ # Post inline review comment
534
+ review_uri = URI("https://api.github.com/repos/#{full_repo}/pulls/#{number}/reviews")
535
+ review_request = Net::HTTP::Post.new(review_uri)
536
+ review_request["Content-Type"] = "application/json"
537
+ review_request["Accept"] = "application/vnd.github.v3+json"
538
+
539
+ review_data = {
540
+ body: body,
541
+ event: "COMMENT",
542
+ comments: [
543
+ {
544
+ path: path,
545
+ line: line,
546
+ body: body
547
+ }
548
+ ]
549
+ }
550
+
551
+ review_request.body = JSON.dump(review_data)
552
+ [review_uri, review_request]
553
+ else
554
+ # Post general comment on the PR
555
+ comment_uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/comments")
556
+ comment_request = Net::HTTP::Post.new(comment_uri)
557
+ comment_request["Content-Type"] = "application/json"
558
+ comment_request.body = JSON.dump({body: body})
559
+ [comment_uri, comment_request]
560
+ end
561
+
562
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
563
+ http.request(request)
564
+ end
565
+
566
+ error_msg = (path && line && commit_id) ? "GitHub API review comment failed (#{response.code}): #{response.body}" : "GitHub API comment failed (#{response.code})"
567
+ raise error_msg unless response.code.start_with?("2")
568
+
569
+ response.body
570
+ end
571
+
572
+ def fetch_pr_comments_via_gh(number)
573
+ cmd = ["gh", "api", "repos/#{full_repo}/issues/#{number}/comments", "--jq", "."]
574
+ stdout, stderr, status = Open3.capture3(*cmd)
575
+ raise "Failed to fetch PR comments via gh: #{stderr.strip}" unless status.success?
576
+
577
+ JSON.parse(stdout).map { |raw| normalize_pr_comment(raw) }
578
+ rescue JSON::ParserError => e
579
+ raise "Failed to parse PR comments response: #{e.message}"
580
+ end
581
+
582
+ def fetch_pr_comments_via_api(number)
583
+ uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/comments")
584
+ response = Net::HTTP.get_response(uri)
585
+ return [] unless response.code == "200"
586
+
587
+ JSON.parse(response.body).map { |raw| normalize_pr_comment(raw) }
588
+ rescue => e
589
+ Aidp.log_warn("repository_client", "Failed to fetch PR comments", error: e.message)
590
+ []
591
+ end
592
+
593
+ # Normalization methods for PRs
594
+ def normalize_pull_request(raw)
595
+ {
596
+ number: raw["number"],
597
+ title: raw["title"],
598
+ labels: Array(raw["labels"]).map { |label| label.is_a?(Hash) ? label["name"] : label },
599
+ updated_at: raw["updatedAt"],
600
+ state: raw["state"],
601
+ url: raw["url"],
602
+ head_ref: raw["headRefName"],
603
+ base_ref: raw["baseRefName"]
604
+ }
605
+ end
606
+
607
+ def normalize_pull_request_api(raw)
608
+ {
609
+ number: raw["number"],
610
+ title: raw["title"],
611
+ labels: Array(raw["labels"]).map { |label| label["name"] },
612
+ updated_at: raw["updated_at"],
613
+ state: raw["state"],
614
+ url: raw["html_url"],
615
+ head_ref: raw.dig("head", "ref"),
616
+ base_ref: raw.dig("base", "ref")
617
+ }
618
+ end
619
+
620
+ def normalize_pull_request_detail(raw)
621
+ {
622
+ number: raw["number"],
623
+ title: raw["title"],
624
+ body: raw["body"] || "",
625
+ author: raw.dig("author", "login") || raw["author"],
626
+ labels: Array(raw["labels"]).map { |label| label.is_a?(Hash) ? label["name"] : label },
627
+ state: raw["state"],
628
+ url: raw["url"],
629
+ head_ref: raw["headRefName"],
630
+ base_ref: raw["baseRefName"],
631
+ head_sha: raw.dig("commits", 0, "oid") || raw["headRefOid"],
632
+ mergeable: raw["mergeable"]
633
+ }
634
+ end
635
+
636
+ def normalize_pull_request_detail_api(raw)
637
+ {
638
+ number: raw["number"],
639
+ title: raw["title"],
640
+ body: raw["body"] || "",
641
+ author: raw.dig("user", "login"),
642
+ labels: Array(raw["labels"]).map { |label| label["name"] },
643
+ state: raw["state"],
644
+ url: raw["html_url"],
645
+ head_ref: raw.dig("head", "ref"),
646
+ base_ref: raw.dig("base", "ref"),
647
+ head_sha: raw.dig("head", "sha"),
648
+ mergeable: raw["mergeable"]
649
+ }
650
+ end
651
+
652
+ def normalize_pr_file(raw)
653
+ {
654
+ filename: raw["filename"],
655
+ status: raw["status"],
656
+ additions: raw["additions"],
657
+ deletions: raw["deletions"],
658
+ changes: raw["changes"],
659
+ patch: raw["patch"]
660
+ }
661
+ end
662
+
663
+ def normalize_ci_status(check_runs, head_sha)
664
+ checks = check_runs.map do |run|
665
+ {
666
+ name: run["name"],
667
+ status: run["status"],
668
+ conclusion: run["conclusion"],
669
+ details_url: run["details_url"],
670
+ output: run["output"]
671
+ }
672
+ end
673
+
674
+ # Determine overall state
675
+ state = if checks.any? { |c| c[:conclusion] == "failure" }
676
+ "failure"
677
+ elsif checks.any? { |c| c[:status] != "completed" }
678
+ "pending"
679
+ elsif checks.all? { |c| c[:conclusion] == "success" }
680
+ "success"
681
+ else
682
+ "unknown"
683
+ end
684
+
685
+ {
686
+ sha: head_sha,
687
+ state: state,
688
+ checks: checks
689
+ }
690
+ end
691
+
257
692
  def normalize_issue(raw)
258
693
  {
259
694
  number: raw["number"],
@@ -311,14 +746,24 @@ module Aidp
311
746
  def normalize_comment(comment)
312
747
  if comment.is_a?(Hash)
313
748
  {
314
- "body" => comment["body"],
315
- "author" => comment["author"] || comment.dig("user", "login"),
316
- "createdAt" => comment["createdAt"] || comment["created_at"]
749
+ "body" => comment["body"] || comment[:body],
750
+ "author" => comment["author"] || comment[:author] || comment.dig("user", "login"),
751
+ "createdAt" => comment["createdAt"] || comment[:created_at] || comment["created_at"]
317
752
  }
318
753
  else
319
754
  {"body" => comment.to_s}
320
755
  end
321
756
  end
757
+
758
+ def normalize_pr_comment(raw)
759
+ {
760
+ id: raw["id"],
761
+ body: raw["body"],
762
+ author: raw.dig("user", "login"),
763
+ created_at: raw["created_at"],
764
+ updated_at: raw["updated_at"]
765
+ }
766
+ end
322
767
  end
323
768
  end
324
769
  end