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.
- checksums.yaml +4 -4
- data/README.md +72 -7
- data/lib/aidp/analyze/error_handler.rb +11 -0
- data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
- data/lib/aidp/auto_update/checkpoint.rb +178 -0
- data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
- data/lib/aidp/auto_update/coordinator.rb +204 -0
- data/lib/aidp/auto_update/errors.rb +17 -0
- data/lib/aidp/auto_update/failure_tracker.rb +162 -0
- data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
- data/lib/aidp/auto_update/update_check.rb +106 -0
- data/lib/aidp/auto_update/update_logger.rb +143 -0
- data/lib/aidp/auto_update/update_policy.rb +109 -0
- data/lib/aidp/auto_update/version_detector.rb +144 -0
- data/lib/aidp/auto_update.rb +52 -0
- data/lib/aidp/cli.rb +165 -1
- 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 +80 -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_factory.rb +2 -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 +56 -2
- data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
- data/lib/aidp/provider_manager.rb +2 -0
- data/lib/aidp/providers/kilocode.rb +202 -0
- data/lib/aidp/safe_directory.rb +10 -3
- data/lib/aidp/setup/provider_registry.rb +15 -0
- data/lib/aidp/setup/wizard.rb +12 -4
- data/lib/aidp/skills/composer.rb +4 -0
- data/lib/aidp/skills/loader.rb +3 -1
- 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 +106 -17
- data/lib/aidp/watch/change_request_processor.rb +659 -0
- data/lib/aidp/watch/ci_fix_processor.rb +448 -0
- data/lib/aidp/watch/plan_processor.rb +81 -8
- data/lib/aidp/watch/repository_client.rb +465 -20
- data/lib/aidp/watch/review_processor.rb +266 -0
- data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
- data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
- data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
- data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
- data/lib/aidp/watch/runner.rb +222 -0
- data/lib/aidp/watch/state_store.rb +99 -1
- data/lib/aidp/workstream_executor.rb +5 -2
- data/lib/aidp.rb +5 -0
- data/templates/aidp.yml.example +53 -0
- 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 =
|
|
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
|