aidp 0.33.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
  4. data/lib/aidp/cli/eval_command.rb +399 -0
  5. data/lib/aidp/cli/harness_command.rb +1 -1
  6. data/lib/aidp/cli/security_command.rb +416 -0
  7. data/lib/aidp/cli/tools_command.rb +6 -4
  8. data/lib/aidp/cli.rb +170 -3
  9. data/lib/aidp/concurrency/exec.rb +3 -0
  10. data/lib/aidp/config.rb +113 -0
  11. data/lib/aidp/config_paths.rb +20 -0
  12. data/lib/aidp/daemon/runner.rb +8 -4
  13. data/lib/aidp/errors.rb +134 -0
  14. data/lib/aidp/evaluations/context_capture.rb +205 -0
  15. data/lib/aidp/evaluations/evaluation_record.rb +114 -0
  16. data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
  17. data/lib/aidp/evaluations.rb +23 -0
  18. data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
  19. data/lib/aidp/execute/interactive_repl.rb +6 -2
  20. data/lib/aidp/execute/prompt_evaluator.rb +359 -0
  21. data/lib/aidp/execute/repl_macros.rb +100 -1
  22. data/lib/aidp/execute/work_loop_runner.rb +399 -47
  23. data/lib/aidp/execute/work_loop_state.rb +4 -1
  24. data/lib/aidp/execute/workflow_selector.rb +3 -0
  25. data/lib/aidp/harness/ai_decision_engine.rb +79 -0
  26. data/lib/aidp/harness/capability_registry.rb +2 -0
  27. data/lib/aidp/harness/condition_detector.rb +3 -0
  28. data/lib/aidp/harness/config_loader.rb +3 -0
  29. data/lib/aidp/harness/enhanced_runner.rb +14 -11
  30. data/lib/aidp/harness/error_handler.rb +3 -0
  31. data/lib/aidp/harness/provider_factory.rb +3 -0
  32. data/lib/aidp/harness/provider_manager.rb +6 -0
  33. data/lib/aidp/harness/runner.rb +5 -1
  34. data/lib/aidp/harness/state/persistence.rb +3 -0
  35. data/lib/aidp/harness/state_manager.rb +3 -0
  36. data/lib/aidp/harness/status_display.rb +28 -20
  37. data/lib/aidp/harness/thinking_depth_manager.rb +32 -32
  38. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
  39. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
  40. data/lib/aidp/harness/ui/error_handler.rb +3 -0
  41. data/lib/aidp/harness/ui/job_monitor.rb +4 -0
  42. data/lib/aidp/harness/ui/navigation/submenu.rb +2 -0
  43. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
  44. data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
  45. data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
  46. data/lib/aidp/harness/user_interface.rb +3 -0
  47. data/lib/aidp/loader.rb +2 -2
  48. data/lib/aidp/logger.rb +3 -0
  49. data/lib/aidp/message_display.rb +31 -0
  50. data/lib/aidp/pr_worktree_manager.rb +18 -6
  51. data/lib/aidp/provider_manager.rb +3 -0
  52. data/lib/aidp/providers/base.rb +2 -0
  53. data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
  54. data/lib/aidp/security/secrets_proxy.rb +328 -0
  55. data/lib/aidp/security/secrets_registry.rb +227 -0
  56. data/lib/aidp/security/trifecta_state.rb +220 -0
  57. data/lib/aidp/security/watch_mode_handler.rb +306 -0
  58. data/lib/aidp/security/work_loop_adapter.rb +277 -0
  59. data/lib/aidp/security.rb +56 -0
  60. data/lib/aidp/setup/wizard.rb +4 -2
  61. data/lib/aidp/version.rb +1 -1
  62. data/lib/aidp/watch/auto_merger.rb +274 -0
  63. data/lib/aidp/watch/auto_pr_processor.rb +125 -7
  64. data/lib/aidp/watch/build_processor.rb +16 -1
  65. data/lib/aidp/watch/change_request_processor.rb +680 -286
  66. data/lib/aidp/watch/ci_fix_processor.rb +262 -4
  67. data/lib/aidp/watch/feedback_collector.rb +191 -0
  68. data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
  69. data/lib/aidp/watch/implementation_verifier.rb +142 -1
  70. data/lib/aidp/watch/plan_generator.rb +70 -13
  71. data/lib/aidp/watch/plan_processor.rb +12 -5
  72. data/lib/aidp/watch/projects_processor.rb +286 -0
  73. data/lib/aidp/watch/repository_client.rb +861 -53
  74. data/lib/aidp/watch/review_processor.rb +33 -6
  75. data/lib/aidp/watch/runner.rb +51 -11
  76. data/lib/aidp/watch/state_store.rb +233 -0
  77. data/lib/aidp/watch/sub_issue_creator.rb +221 -0
  78. data/lib/aidp/workflows/guided_agent.rb +4 -0
  79. data/lib/aidp/workstream_executor.rb +3 -0
  80. data/lib/aidp/worktree.rb +61 -11
  81. data/lib/aidp/worktree_branch_manager.rb +347 -101
  82. data/templates/implementation/iterative_implementation.md +46 -3
  83. metadata +20 -1
@@ -26,9 +26,9 @@ module Aidp
26
26
  def self.parse_issues_url(issues_url)
27
27
  case issues_url
28
28
  when %r{\Ahttps://github\.com/([^/]+)/([^/]+)(?:/issues)?/?\z}
29
- [$1, $2]
29
+ [::Regexp.last_match(1), ::Regexp.last_match(2)]
30
30
  when %r{\A([^/]+)/([^/]+)\z}
31
- [$1, $2]
31
+ [::Regexp.last_match(1), ::Regexp.last_match(2)]
32
32
  else
33
33
  raise ArgumentError, "Unsupported issues URL: #{issues_url}"
34
34
  end
@@ -50,7 +50,14 @@ module Aidp
50
50
  end
51
51
 
52
52
  def list_issues(labels: [], state: "open")
53
- gh_available? ? list_issues_via_gh(labels: labels, state: state) : list_issues_via_api(labels: labels, state: state)
53
+ if gh_available?
54
+ list_issues_via_gh(labels: labels,
55
+ state: state)
56
+ else
57
+ list_issues_via_api(
58
+ labels: labels, state: state
59
+ )
60
+ end
54
61
  end
55
62
 
56
63
  def fetch_issue(number)
@@ -70,7 +77,10 @@ module Aidp
70
77
  end
71
78
 
72
79
  def create_pull_request(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil)
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")
80
+ raise("GitHub CLI not available - cannot create PR") unless gh_available?
81
+
82
+ create_pull_request_via_gh(title: title, body: body, head: head, base: base,
83
+ issue_number: issue_number, draft: draft, assignee: assignee)
74
84
  end
75
85
 
76
86
  def add_labels(number, *labels)
@@ -109,56 +119,198 @@ module Aidp
109
119
  end
110
120
 
111
121
  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)
122
+ if gh_available?
123
+ post_review_comment_via_gh(number, body, commit_id: commit_id, path: path,
124
+ line: line)
125
+ else
126
+ post_review_comment_via_api(number,
127
+ body, commit_id: commit_id, path: path, line: line)
128
+ end
113
129
  end
114
130
 
115
131
  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)
132
+ if gh_available?
133
+ list_pull_requests_via_gh(labels: labels,
134
+ state: state)
135
+ else
136
+ list_pull_requests_via_api(
137
+ labels: labels, state: state
138
+ )
139
+ end
117
140
  end
118
141
 
119
142
  def fetch_pr_comments(number)
120
143
  gh_available? ? fetch_pr_comments_via_gh(number) : fetch_pr_comments_via_api(number)
121
144
  end
122
145
 
146
+ # Convert a draft PR to ready for review
147
+ # @param number [Integer] PR number
148
+ # @return [Boolean] True if successful
149
+ def mark_pr_ready_for_review(number)
150
+ raise("GitHub CLI not available - cannot mark PR ready") unless gh_available?
151
+ mark_pr_ready_for_review_via_gh(number)
152
+ end
153
+
154
+ # Request reviewers for a PR
155
+ # @param number [Integer] PR number
156
+ # @param reviewers [Array<String>] GitHub usernames to request as reviewers
157
+ # @return [Boolean] True if successful
158
+ def request_reviewers(number, reviewers:)
159
+ return true if reviewers.nil? || reviewers.empty?
160
+ raise("GitHub CLI not available - cannot request reviewers") unless gh_available?
161
+ request_reviewers_via_gh(number, reviewers: reviewers)
162
+ end
163
+
164
+ # Get the actor who most recently added a label to a PR
165
+ # @param number [Integer] PR number
166
+ # @return [String, nil] GitHub username or nil
167
+ def most_recent_pr_label_actor(number)
168
+ gh_available? ? most_recent_pr_label_actor_via_gh(number) : nil
169
+ end
170
+
171
+ # Fetch reactions on a specific comment
172
+ # Returns array of reactions with user and content (emoji type)
173
+ def fetch_comment_reactions(comment_id)
174
+ gh_available? ? fetch_comment_reactions_via_gh(comment_id) : fetch_comment_reactions_via_api(comment_id)
175
+ end
176
+
123
177
  # Create or update a categorized comment (e.g., under a header) on an issue.
124
178
  # If a comment with the category header exists, either append to it or
125
179
  # replace it while archiving the previous content inline.
126
- def consolidate_category_comment(issue_number, category_header, content, append: false)
127
- existing_comment = find_comment(issue_number, category_header)
128
-
129
- if existing_comment.nil?
130
- Aidp.log_debug("repository_client", "creating_category_comment",
131
- issue: issue_number,
132
- header: category_header)
133
- return post_comment(issue_number, "#{category_header}\n\n#{content}")
134
- end
135
-
136
- existing_body = existing_comment[:body] || existing_comment["body"] || ""
137
- content_without_header = existing_body.sub(/\A#{Regexp.escape(category_header)}\s*/, "").strip
180
+ def consolidate_category_comment(number, category_header, content, append: false)
181
+ Aidp.log_debug(
182
+ "repository_client",
183
+ "consolidate_category_comment_started",
184
+ number: number,
185
+ category_header: category_header,
186
+ append: append,
187
+ content_length: content.length,
188
+ content_preview: content[0, 100]
189
+ )
138
190
 
139
- new_body =
140
- if append
141
- Aidp.log_debug("repository_client", "appending_category_comment",
142
- issue: issue_number,
143
- header: category_header)
144
- segments = [category_header, content_without_header, content].reject(&:empty?)
145
- segments.join("\n\n")
191
+ existing_comment = find_comment(number, category_header)
192
+
193
+ if existing_comment
194
+ body = if append
195
+ Aidp.log_debug(
196
+ "repository_client",
197
+ "updating_category_comment_appending",
198
+ comment_id: existing_comment[:id],
199
+ existing_body_length: existing_comment[:body].length,
200
+ existing_body_preview: existing_comment[:body][0, 100],
201
+ appending_content_length: content.length,
202
+ appending_content_preview: content[0, 100]
203
+ )
204
+ "#{existing_comment[:body]}\n\n#{content}"
146
205
  else
147
- Aidp.log_debug("repository_client", "replacing_category_comment",
148
- issue: issue_number,
149
- header: category_header)
150
- timestamp = Time.now.utc.iso8601
151
- archive_marker = "<!-- ARCHIVED_PLAN_START #{timestamp} ARCHIVED_PLAN_END -->"
152
- [category_header, content, archive_marker, content_without_header].join("\n\n")
206
+ Aidp.log_debug(
207
+ "repository_client",
208
+ "updating_category_comment_replacing",
209
+ comment_id: existing_comment[:id],
210
+ existing_body_length: existing_comment[:body].length,
211
+ existing_body_preview: existing_comment[:body][0, 100],
212
+ replacement_content_length: content.length,
213
+ replacement_content_preview: content[0, 100]
214
+ )
215
+
216
+ archived_prefix = "<!-- ARCHIVED_PLAN_START "
217
+ archived_suffix = " ARCHIVED_PLAN_END -->"
218
+ archived_content = "#{archived_prefix}#{Time.now.utc.iso8601}#{archived_suffix}\n\n#{existing_comment[:body].gsub(
219
+ /^(#{Regexp.escape(category_header)}|#{Regexp.escape(archived_prefix)}.*?#{Regexp.escape(archived_suffix)})/m, ""
220
+ )}\n\n"
221
+
222
+ "#{category_header}\n\n#{archived_content}#{content}"
153
223
  end
154
224
 
155
- update_comment(existing_comment[:id] || existing_comment["id"], new_body)
225
+ update_comment(existing_comment[:id], body)
226
+
227
+ Aidp.log_debug(
228
+ "repository_client",
229
+ "existing_category_comment_updated",
230
+ comment_id: existing_comment[:id],
231
+ updated_body_length: body.length,
232
+ updated_body_preview: body[0, 100],
233
+ update_method: append ? "append" : "replace"
234
+ )
235
+
236
+ {
237
+ id: existing_comment[:id],
238
+ body: body
239
+ }
240
+ else
241
+ body = "#{category_header}\n\n#{content}"
242
+
243
+ post_comment(number, body)
244
+
245
+ Aidp.log_debug(
246
+ "repository_client",
247
+ "new_category_comment_created",
248
+ issue_number: number,
249
+ body_length: body.length,
250
+ body_preview: body[0, 100],
251
+ category_header: category_header
252
+ )
253
+
254
+ {
255
+ id: 999,
256
+ body: body
257
+ }
258
+ end
156
259
  rescue => e
157
- Aidp.log_error("repository_client", "consolidate_category_comment_failed",
158
- issue: issue_number,
159
- header: category_header,
160
- error: e.message)
161
- raise "GitHub error: #{e.message}"
260
+ Aidp.log_error(
261
+ "repository_client",
262
+ "consolidate_category_comment_failed",
263
+ error: e.message,
264
+ error_class: e.class.name,
265
+ number: number,
266
+ category_header: category_header,
267
+ content_length: content.length,
268
+ content_preview: content[0, 100],
269
+ backtrace: e.backtrace&.first(5)
270
+ )
271
+
272
+ raise RuntimeError, "GitHub error", e.backtrace
273
+ end
274
+
275
+ # GitHub Projects V2 operations
276
+ def fetch_project(project_id)
277
+ raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
278
+ fetch_project_via_gh(project_id)
279
+ end
280
+
281
+ def list_project_items(project_id)
282
+ raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
283
+ list_project_items_via_gh(project_id)
284
+ end
285
+
286
+ def link_issue_to_project(project_id, issue_number)
287
+ raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
288
+ link_issue_to_project_via_gh(project_id, issue_number)
289
+ end
290
+
291
+ def update_project_item_field(item_id, field_id, value)
292
+ raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
293
+ update_project_item_field_via_gh(item_id, field_id, value)
294
+ end
295
+
296
+ def fetch_project_fields(project_id)
297
+ raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
298
+ fetch_project_fields_via_gh(project_id)
299
+ end
300
+
301
+ def create_project_field(project_id, name, field_type, options: nil)
302
+ raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
303
+ create_project_field_via_gh(project_id, name, field_type, options: options)
304
+ end
305
+
306
+ def create_issue(title:, body:, labels: [], assignees: [])
307
+ raise "GitHub CLI not available - cannot create issue" unless gh_available?
308
+ create_issue_via_gh(title: title, body: body, labels: labels, assignees: assignees)
309
+ end
310
+
311
+ def merge_pull_request(number, merge_method: "squash")
312
+ raise "GitHub CLI not available - cannot merge PR" unless gh_available?
313
+ merge_pull_request_via_gh(number, merge_method: merge_method)
162
314
  end
163
315
 
164
316
  private
@@ -221,7 +373,10 @@ module Aidp
221
373
  def list_issues_via_api(labels:, state:)
222
374
  label_param = labels.join(",")
223
375
  uri = URI("https://api.github.com/repos/#{full_repo}/issues?state=#{state}")
224
- uri.query = [uri.query, "labels=#{URI.encode_www_form_component(label_param)}"].compact.join("&") unless label_param.empty?
376
+ unless label_param.empty?
377
+ uri.query = [uri.query,
378
+ "labels=#{URI.encode_www_form_component(label_param)}"].compact.join("&")
379
+ end
225
380
 
226
381
  response = Net::HTTP.get_response(uri)
227
382
  return [] unless response.code == "200"
@@ -259,11 +414,20 @@ module Aidp
259
414
  end
260
415
 
261
416
  def post_comment_via_gh(number, body)
262
- cmd = ["gh", "issue", "comment", number.to_s, "--repo", full_repo, "--body", body]
263
- stdout, stderr, status = Open3.capture3(*cmd)
264
- raise "Failed to post comment via gh: #{stderr.strip}" unless status.success?
417
+ # Use gh api to post comment and get structured response with comment ID
418
+ with_gh_retry("post_comment") do
419
+ cmd = ["gh", "api", "repos/#{full_repo}/issues/#{number}/comments",
420
+ "-X", "POST", "-f", "body=#{body}"]
421
+ stdout, stderr, status = Open3.capture3(*cmd)
422
+ raise "Failed to post comment via gh: #{stderr.strip}" unless status.success?
265
423
 
266
- stdout.strip
424
+ response = JSON.parse(stdout)
425
+ {
426
+ id: response["id"],
427
+ url: response["html_url"],
428
+ body: response["body"]
429
+ }
430
+ end
267
431
  end
268
432
 
269
433
  def post_comment_via_api(number, body)
@@ -277,7 +441,13 @@ module Aidp
277
441
  end
278
442
 
279
443
  raise "GitHub API comment failed (#{response.code})" unless response.code.start_with?("2")
280
- response.body
444
+
445
+ data = JSON.parse(response.body)
446
+ {
447
+ id: data["id"],
448
+ url: data["html_url"],
449
+ body: data["body"]
450
+ }
281
451
  end
282
452
 
283
453
  def find_comment_via_gh(number, header_text)
@@ -315,9 +485,57 @@ module Aidp
315
485
  end
316
486
 
317
487
  raise "GitHub API update comment failed (#{response.code})" unless response.code.start_with?("2")
488
+
318
489
  response.body
319
490
  end
320
491
 
492
+ def fetch_comment_reactions_via_gh(comment_id)
493
+ with_gh_retry("fetch_comment_reactions") do
494
+ cmd = ["gh", "api", "repos/#{full_repo}/issues/comments/#{comment_id}/reactions"]
495
+ stdout, stderr, status = Open3.capture3(*cmd)
496
+ raise "Failed to fetch reactions via gh: #{stderr.strip}" unless status.success?
497
+
498
+ reactions = JSON.parse(stdout)
499
+ reactions.map do |r|
500
+ {
501
+ id: r["id"],
502
+ user: r.dig("user", "login"),
503
+ content: r["content"],
504
+ created_at: r["created_at"]
505
+ }
506
+ end
507
+ end
508
+ rescue => e
509
+ Aidp.log_error("repository_client", "fetch_reactions_failed", comment_id: comment_id, error: e.message)
510
+ []
511
+ end
512
+
513
+ def fetch_comment_reactions_via_api(comment_id)
514
+ uri = URI("https://api.github.com/repos/#{full_repo}/issues/comments/#{comment_id}/reactions")
515
+ request = Net::HTTP::Get.new(uri)
516
+ # Reactions API requires special Accept header
517
+ request["Accept"] = "application/vnd.github+json"
518
+
519
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
520
+ http.request(request)
521
+ end
522
+
523
+ return [] unless response.code.start_with?("2")
524
+
525
+ reactions = JSON.parse(response.body)
526
+ reactions.map do |r|
527
+ {
528
+ id: r["id"],
529
+ user: r.dig("user", "login"),
530
+ content: r["content"],
531
+ created_at: r["created_at"]
532
+ }
533
+ end
534
+ rescue => e
535
+ Aidp.log_error("repository_client", "fetch_reactions_api_failed", comment_id: comment_id, error: e.message)
536
+ []
537
+ end
538
+
321
539
  def create_pull_request_via_gh(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil)
322
540
  Aidp.log_debug(
323
541
  "repository_client",
@@ -436,6 +654,7 @@ module Aidp
436
654
  end
437
655
 
438
656
  raise "Failed to add labels via API (#{response.code})" unless response.code.start_with?("2")
657
+
439
658
  response.body
440
659
  end
441
660
 
@@ -546,7 +765,10 @@ module Aidp
546
765
  def list_pull_requests_via_api(labels:, state:)
547
766
  label_param = labels.join(",")
548
767
  uri = URI("https://api.github.com/repos/#{full_repo}/pulls?state=#{state}")
549
- uri.query = [uri.query, "labels=#{URI.encode_www_form_component(label_param)}"].compact.join("&") unless label_param.empty?
768
+ unless label_param.empty?
769
+ uri.query = [uri.query,
770
+ "labels=#{URI.encode_www_form_component(label_param)}"].compact.join("&")
771
+ end
550
772
 
551
773
  response = Net::HTTP.get_response(uri)
552
774
  return [] unless response.code == "200"
@@ -558,7 +780,7 @@ module Aidp
558
780
  end
559
781
 
560
782
  def fetch_pull_request_via_gh(number)
561
- fields = %w[number title body labels state url headRefName baseRefName commits author mergeable]
783
+ fields = %w[number title body labels state url headRefName baseRefName headRefOid commits author mergeable mergeStateStatus]
562
784
  cmd = ["gh", "pr", "view", number.to_s, "--repo", full_repo, "--json", fields.join(",")]
563
785
 
564
786
  stdout, stderr, status = Open3.capture3(*cmd)
@@ -597,6 +819,7 @@ module Aidp
597
819
  end
598
820
 
599
821
  raise "GitHub API diff failed (#{response.code})" unless response.code == "200"
822
+
600
823
  response.body
601
824
  end
602
825
 
@@ -664,7 +887,8 @@ module Aidp
664
887
  data = JSON.parse(response.body)
665
888
  data["check_runs"] || []
666
889
  else
667
- Aidp.log_warn("repository_client", "Failed to fetch check runs via API", sha: head_sha, code: response.code)
890
+ Aidp.log_warn("repository_client", "Failed to fetch check runs via API", sha: head_sha,
891
+ code: response.code)
668
892
  []
669
893
  end
670
894
 
@@ -680,7 +904,7 @@ module Aidp
680
904
 
681
905
  def post_review_comment_via_gh(number, body, commit_id: nil, path: nil, line: nil)
682
906
  if path && line && commit_id
683
- # Note: gh CLI doesn't support inline comments directly, so we use the API
907
+ # NOTE: gh CLI doesn't support inline comments directly, so we use the API
684
908
  # For inline comments, we need to use the GitHub API
685
909
  post_review_comment_via_api(number, body, commit_id: commit_id, path: path, line: line)
686
910
  else
@@ -755,6 +979,98 @@ module Aidp
755
979
  []
756
980
  end
757
981
 
982
+ def mark_pr_ready_for_review_via_gh(number)
983
+ cmd = ["gh", "pr", "ready", number.to_s, "--repo", full_repo]
984
+ _stdout, stderr, status = Open3.capture3(*cmd)
985
+
986
+ unless status.success?
987
+ Aidp.log_warn("repository_client", "mark_pr_ready_failed",
988
+ pr: number, stderr: stderr.strip)
989
+ return false
990
+ end
991
+
992
+ Aidp.log_info("repository_client", "pr_marked_ready", pr: number)
993
+ true
994
+ rescue => e
995
+ Aidp.log_error("repository_client", "mark_pr_ready_exception",
996
+ pr: number, error: e.message)
997
+ false
998
+ end
999
+
1000
+ def request_reviewers_via_gh(number, reviewers:)
1001
+ reviewer_args = reviewers.flat_map { |r| ["--add-reviewer", r] }
1002
+ cmd = ["gh", "pr", "edit", number.to_s, "--repo", full_repo] + reviewer_args
1003
+ _stdout, stderr, status = Open3.capture3(*cmd)
1004
+
1005
+ unless status.success?
1006
+ Aidp.log_warn("repository_client", "request_reviewers_failed",
1007
+ pr: number, reviewers: reviewers, stderr: stderr.strip)
1008
+ return false
1009
+ end
1010
+
1011
+ Aidp.log_info("repository_client", "reviewers_requested",
1012
+ pr: number, reviewers: reviewers)
1013
+ true
1014
+ rescue => e
1015
+ Aidp.log_error("repository_client", "request_reviewers_exception",
1016
+ pr: number, reviewers: reviewers, error: e.message)
1017
+ false
1018
+ end
1019
+
1020
+ def most_recent_pr_label_actor_via_gh(number)
1021
+ # Use GitHub GraphQL API to fetch the most recent label event actor for a PR
1022
+ query = <<~GRAPHQL
1023
+ query($owner: String!, $repo: String!, $number: Int!) {
1024
+ repository(owner: $owner, name: $repo) {
1025
+ pullRequest(number: $number) {
1026
+ timelineItems(last: 100, itemTypes: [LABELED_EVENT]) {
1027
+ nodes {
1028
+ ... on LabeledEvent {
1029
+ createdAt
1030
+ actor {
1031
+ login
1032
+ }
1033
+ }
1034
+ }
1035
+ }
1036
+ }
1037
+ }
1038
+ }
1039
+ GRAPHQL
1040
+
1041
+ cmd = [
1042
+ "gh", "api", "graphql",
1043
+ "-f", "query=#{query}",
1044
+ "-F", "owner=#{owner}",
1045
+ "-F", "repo=#{repo}",
1046
+ "-F", "number=#{number}"
1047
+ ]
1048
+
1049
+ stdout, stderr, status = Open3.capture3(*cmd)
1050
+ unless status.success?
1051
+ Aidp.log_warn("repository_client", "pr_label_actor_query_failed",
1052
+ pr: number, error: stderr.strip)
1053
+ return nil
1054
+ end
1055
+
1056
+ data = JSON.parse(stdout)
1057
+ events = data.dig("data", "repository", "pullRequest", "timelineItems", "nodes") || []
1058
+
1059
+ valid_events = events.select { |event| event.dig("actor", "login") }
1060
+ return nil if valid_events.empty?
1061
+
1062
+ most_recent = valid_events.max_by { |event| event["createdAt"] }
1063
+ most_recent.dig("actor", "login")
1064
+ rescue JSON::ParserError => e
1065
+ Aidp.log_warn("repository_client", "pr_label_actor_parse_failed",
1066
+ pr: number, error: e.message)
1067
+ nil
1068
+ rescue => e
1069
+ Aidp.log_warn("repository_client", "pr_label_actor_exception",
1070
+ pr: number, error: e.message)
1071
+ nil
1072
+ end
1073
+
758
1074
  # Normalization methods for PRs
759
1075
  def normalize_pull_request(raw)
760
1076
  {
@@ -793,8 +1109,9 @@ module Aidp
793
1109
  url: raw["url"],
794
1110
  head_ref: raw["headRefName"],
795
1111
  base_ref: raw["baseRefName"],
796
- head_sha: raw.dig("commits", 0, "oid") || raw["headRefOid"],
797
- mergeable: raw["mergeable"]
1112
+ head_sha: raw["headRefOid"] || raw.dig("commits", 0, "oid"),
1113
+ mergeable: raw["mergeable"],
1114
+ merge_state_status: raw["mergeStateStatus"]&.downcase
798
1115
  }
799
1116
  end
800
1117
 
@@ -810,7 +1127,8 @@ module Aidp
810
1127
  head_ref: raw.dig("head", "ref"),
811
1128
  base_ref: raw.dig("base", "ref"),
812
1129
  head_sha: raw.dig("head", "sha"),
813
- mergeable: raw["mergeable"]
1130
+ mergeable: raw["mergeable"],
1131
+ merge_state_status: raw["merge_state_status"]&.downcase
814
1132
  }
815
1133
  end
816
1134
 
@@ -863,6 +1181,13 @@ module Aidp
863
1181
  end
864
1182
 
865
1183
  def normalize_ci_status_combined(check_runs, commit_statuses, head_sha)
1184
+ # Log raw inputs for debugging
1185
+ Aidp.log_debug("repository_client", "normalize_ci_status_combined_raw_inputs",
1186
+ check_run_count: check_runs.length,
1187
+ commit_status_count: commit_statuses.length,
1188
+ check_runs_sample: check_runs.first(3).map { |cr| {name: cr["name"], status: cr["status"], conclusion: cr["conclusion"]} },
1189
+ commit_statuses_sample: commit_statuses.first(3).map { |cs| {context: cs["context"], state: cs["state"]} })
1190
+
866
1191
  # Convert commit statuses to same format as check runs for unified processing
867
1192
  # normalize_ci_status expects string keys, so we use string keys here
868
1193
  checks_from_statuses = commit_statuses.map do |status|
@@ -882,7 +1207,8 @@ module Aidp
882
1207
  Aidp.log_debug("repository_client", "combined_ci_checks",
883
1208
  check_run_count: check_runs.length,
884
1209
  commit_status_count: commit_statuses.length,
885
- total_checks: all_checks.length)
1210
+ total_checks: all_checks.length,
1211
+ combined_checks_sample: all_checks.first(5).map { |c| {name: c["name"], status: c["status"], conclusion: c["conclusion"]} })
886
1212
 
887
1213
  # Use existing normalize logic
888
1214
  normalize_ci_status(all_checks, head_sha)
@@ -905,6 +1231,12 @@ module Aidp
905
1231
  end
906
1232
 
907
1233
  def normalize_ci_status(check_runs, head_sha)
1234
+ # Log raw input data for debugging
1235
+ Aidp.log_debug("repository_client", "normalize_ci_status_raw_input",
1236
+ sha: head_sha,
1237
+ raw_check_run_count: check_runs.length,
1238
+ raw_checks_detailed: check_runs.map { |r| {name: r["name"], status: r["status"], conclusion: r["conclusion"]} })
1239
+
908
1240
  checks = check_runs.map do |run|
909
1241
  {
910
1242
  name: run["name"],
@@ -915,7 +1247,7 @@ module Aidp
915
1247
  }
916
1248
  end
917
1249
 
918
- Aidp.log_debug("repository_client", "normalize_ci_status",
1250
+ Aidp.log_debug("repository_client", "normalize_ci_status_normalized",
919
1251
  check_count: checks.length,
920
1252
  checks: checks.map { |c| {name: c[:name], status: c[:status], conclusion: c[:conclusion]} })
921
1253
 
@@ -945,7 +1277,9 @@ module Aidp
945
1277
  non_success_checks = checks.reject { |c| c[:conclusion] == "success" }
946
1278
  Aidp.log_debug("repository_client", "ci_status_unknown",
947
1279
  non_success_count: non_success_checks.length,
948
- non_success_checks: non_success_checks.map { |c| {name: c[:name], conclusion: c[:conclusion]} })
1280
+ non_success_checks: non_success_checks.map do |c|
1281
+ {name: c[:name], conclusion: c[:conclusion]}
1282
+ end)
949
1283
  "unknown"
950
1284
  end
951
1285
 
@@ -1033,6 +1367,480 @@ module Aidp
1033
1367
  updated_at: raw["updated_at"]
1034
1368
  }
1035
1369
  end
1370
+
1371
+ # GitHub Projects V2 API implementations
1372
+ def fetch_project_via_gh(project_id)
1373
+ Aidp.log_debug("repository_client", "fetch_project", project_id: project_id)
1374
+
1375
+ query = <<~GRAPHQL
1376
+ query($projectId: ID!) {
1377
+ node(id: $projectId) {
1378
+ ... on ProjectV2 {
1379
+ id
1380
+ title
1381
+ number
1382
+ url
1383
+ fields(first: 100) {
1384
+ nodes {
1385
+ ... on ProjectV2Field {
1386
+ id
1387
+ name
1388
+ dataType
1389
+ }
1390
+ ... on ProjectV2SingleSelectField {
1391
+ id
1392
+ name
1393
+ dataType
1394
+ options {
1395
+ id
1396
+ name
1397
+ }
1398
+ }
1399
+ }
1400
+ }
1401
+ }
1402
+ }
1403
+ }
1404
+ GRAPHQL
1405
+
1406
+ result = execute_graphql_query(query, projectId: project_id)
1407
+ project_data = result.dig("data", "node")
1408
+
1409
+ unless project_data
1410
+ Aidp.log_warn("repository_client", "Project not found", project_id: project_id)
1411
+ raise "Project not found: #{project_id}"
1412
+ end
1413
+
1414
+ normalize_project(project_data)
1415
+ rescue => e
1416
+ Aidp.log_error("repository_client", "Failed to fetch project", project_id: project_id, error: e.message)
1417
+ raise
1418
+ end
1419
+
1420
+ def list_project_items_via_gh(project_id)
1421
+ Aidp.log_debug("repository_client", "list_project_items", project_id: project_id)
1422
+
1423
+ query = <<~GRAPHQL
1424
+ query($projectId: ID!, $cursor: String) {
1425
+ node(id: $projectId) {
1426
+ ... on ProjectV2 {
1427
+ items(first: 100, after: $cursor) {
1428
+ pageInfo {
1429
+ hasNextPage
1430
+ endCursor
1431
+ }
1432
+ nodes {
1433
+ id
1434
+ type
1435
+ content {
1436
+ ... on Issue {
1437
+ number
1438
+ title
1439
+ state
1440
+ url
1441
+ }
1442
+ ... on PullRequest {
1443
+ number
1444
+ title
1445
+ state
1446
+ url
1447
+ }
1448
+ }
1449
+ fieldValues(first: 100) {
1450
+ nodes {
1451
+ ... on ProjectV2ItemFieldTextValue {
1452
+ text
1453
+ field {
1454
+ ... on ProjectV2Field {
1455
+ id
1456
+ name
1457
+ }
1458
+ }
1459
+ }
1460
+ ... on ProjectV2ItemFieldSingleSelectValue {
1461
+ name
1462
+ field {
1463
+ ... on ProjectV2SingleSelectField {
1464
+ id
1465
+ name
1466
+ }
1467
+ }
1468
+ }
1469
+ }
1470
+ }
1471
+ }
1472
+ }
1473
+ }
1474
+ }
1475
+ }
1476
+ GRAPHQL
1477
+
1478
+ all_items = []
1479
+ cursor = nil
1480
+ has_next_page = true
1481
+
1482
+ while has_next_page
1483
+ variables = {projectId: project_id}
1484
+ variables[:cursor] = cursor if cursor
1485
+
1486
+ result = execute_graphql_query(query, **variables)
1487
+ items_data = result.dig("data", "node", "items")
1488
+
1489
+ break unless items_data
1490
+
1491
+ items = items_data["nodes"] || []
1492
+ all_items.concat(items.map { |item| normalize_project_item(item) })
1493
+
1494
+ page_info = items_data["pageInfo"]
1495
+ has_next_page = page_info["hasNextPage"]
1496
+ cursor = page_info["endCursor"]
1497
+ end
1498
+
1499
+ Aidp.log_debug("repository_client", "list_project_items_complete", project_id: project_id, count: all_items.size)
1500
+ all_items
1501
+ rescue => e
1502
+ Aidp.log_error("repository_client", "Failed to list project items", project_id: project_id, error: e.message)
1503
+ raise
1504
+ end
1505
+
1506
+ def link_issue_to_project_via_gh(project_id, issue_number)
1507
+ Aidp.log_debug("repository_client", "link_issue_to_project", project_id: project_id, issue_number: issue_number)
1508
+
1509
+ # First, get the issue's node ID
1510
+ issue_query = <<~GRAPHQL
1511
+ query($owner: String!, $repo: String!, $number: Int!) {
1512
+ repository(owner: $owner, name: $repo) {
1513
+ issue(number: $number) {
1514
+ id
1515
+ }
1516
+ }
1517
+ }
1518
+ GRAPHQL
1519
+
1520
+ issue_result = execute_graphql_query(issue_query, owner: owner, repo: repo, number: issue_number)
1521
+ issue_id = issue_result.dig("data", "repository", "issue", "id")
1522
+
1523
+ unless issue_id
1524
+ raise "Issue ##{issue_number} not found in #{full_repo}"
1525
+ end
1526
+
1527
+ # Now add the issue to the project
1528
+ mutation = <<~GRAPHQL
1529
+ mutation($projectId: ID!, $contentId: ID!) {
1530
+ addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
1531
+ item {
1532
+ id
1533
+ }
1534
+ }
1535
+ }
1536
+ GRAPHQL
1537
+
1538
+ result = execute_graphql_query(mutation, projectId: project_id, contentId: issue_id)
1539
+ item_id = result.dig("data", "addProjectV2ItemById", "item", "id")
1540
+
1541
+ Aidp.log_debug("repository_client", "link_issue_to_project_complete", project_id: project_id, issue_number: issue_number, item_id: item_id)
1542
+ item_id
1543
+ rescue => e
1544
+ Aidp.log_error("repository_client", "Failed to link issue to project", project_id: project_id, issue_number: issue_number, error: e.message)
1545
+ raise
1546
+ end
1547
+
1548
+ def update_project_item_field_via_gh(item_id, field_id, value)
1549
+ Aidp.log_debug("repository_client", "update_project_item_field", item_id: item_id, field_id: field_id, value: value)
1550
+
1551
+ # Determine the mutation based on value type
1552
+ mutation = if value.is_a?(Hash) && value[:option_id]
1553
+ # Single select field
1554
+ <<~GRAPHQL
1555
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
1556
+ updateProjectV2ItemFieldValue(input: {
1557
+ projectId: $projectId
1558
+ itemId: $itemId
1559
+ fieldId: $fieldId
1560
+ value: {singleSelectOptionId: $optionId}
1561
+ }) {
1562
+ projectV2Item {
1563
+ id
1564
+ }
1565
+ }
1566
+ }
1567
+ GRAPHQL
1568
+ else
1569
+ # Text field
1570
+ <<~GRAPHQL
1571
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $text: String!) {
1572
+ updateProjectV2ItemFieldValue(input: {
1573
+ projectId: $projectId
1574
+ itemId: $itemId
1575
+ fieldId: $fieldId
1576
+ value: {text: $text}
1577
+ }) {
1578
+ projectV2Item {
1579
+ id
1580
+ }
1581
+ }
1582
+ }
1583
+ GRAPHQL
1584
+ end
1585
+
1586
+ # Note: We need the project ID for the mutation
1587
+ # For now, we'll require it to be passed in the value hash
1588
+ project_id = value.is_a?(Hash) ? value[:project_id] : nil
1589
+ raise "project_id required in value hash" unless project_id
1590
+
1591
+ variables = {
1592
+ projectId: project_id,
1593
+ itemId: item_id,
1594
+ fieldId: field_id
1595
+ }
1596
+
1597
+ if value.is_a?(Hash) && value[:option_id]
1598
+ variables[:optionId] = value[:option_id]
1599
+ else
1600
+ variables[:text] = value.to_s
1601
+ end
1602
+
1603
+ result = execute_graphql_query(mutation, **variables)
1604
+ success = result.dig("data", "updateProjectV2ItemFieldValue", "projectV2Item", "id")
1605
+
1606
+ Aidp.log_debug("repository_client", "update_project_item_field_complete", item_id: item_id, field_id: field_id, success: !success.nil?)
1607
+ success
1608
+ rescue => e
1609
+ Aidp.log_error("repository_client", "Failed to update project item field", item_id: item_id, field_id: field_id, error: e.message)
1610
+ raise
1611
+ end
1612
+
1613
+ def fetch_project_fields_via_gh(project_id)
1614
+ Aidp.log_debug("repository_client", "fetch_project_fields", project_id: project_id)
1615
+
1616
+ query = <<~GRAPHQL
1617
+ query($projectId: ID!) {
1618
+ node(id: $projectId) {
1619
+ ... on ProjectV2 {
1620
+ fields(first: 100) {
1621
+ nodes {
1622
+ ... on ProjectV2Field {
1623
+ id
1624
+ name
1625
+ dataType
1626
+ }
1627
+ ... on ProjectV2SingleSelectField {
1628
+ id
1629
+ name
1630
+ dataType
1631
+ options {
1632
+ id
1633
+ name
1634
+ }
1635
+ }
1636
+ }
1637
+ }
1638
+ }
1639
+ }
1640
+ }
1641
+ GRAPHQL
1642
+
1643
+ result = execute_graphql_query(query, projectId: project_id)
1644
+ fields_data = result.dig("data", "node", "fields", "nodes") || []
1645
+
1646
+ fields = fields_data.map { |field| normalize_project_field(field) }
1647
+ Aidp.log_debug("repository_client", "fetch_project_fields_complete", project_id: project_id, count: fields.size)
1648
+ fields
1649
+ rescue => e
1650
+ Aidp.log_error("repository_client", "Failed to fetch project fields", project_id: project_id, error: e.message)
1651
+ raise
1652
+ end
1653
+
1654
+ def create_project_field_via_gh(project_id, name, field_type, options: nil)
1655
+ Aidp.log_debug("repository_client", "create_project_field", project_id: project_id, name: name, field_type: field_type)
1656
+
1657
+ mutation = if field_type == "SINGLE_SELECT" && options
1658
+ <<~GRAPHQL
1659
+ mutation($projectId: ID!, $name: String!, $dataType: ProjectV2CustomFieldType!, $options: [ProjectV2SingleSelectFieldOptionInput!]!) {
1660
+ createProjectV2Field(input: {
1661
+ projectId: $projectId
1662
+ dataType: $dataType
1663
+ name: $name
1664
+ singleSelectOptions: $options
1665
+ }) {
1666
+ projectV2Field {
1667
+ ... on ProjectV2SingleSelectField {
1668
+ id
1669
+ name
1670
+ dataType
1671
+ options {
1672
+ id
1673
+ name
1674
+ }
1675
+ }
1676
+ }
1677
+ }
1678
+ }
1679
+ GRAPHQL
1680
+ else
1681
+ <<~GRAPHQL
1682
+ mutation($projectId: ID!, $name: String!, $dataType: ProjectV2CustomFieldType!) {
1683
+ createProjectV2Field(input: {
1684
+ projectId: $projectId
1685
+ dataType: $dataType
1686
+ name: $name
1687
+ }) {
1688
+ projectV2Field {
1689
+ ... on ProjectV2Field {
1690
+ id
1691
+ name
1692
+ dataType
1693
+ }
1694
+ }
1695
+ }
1696
+ }
1697
+ GRAPHQL
1698
+ end
1699
+
1700
+ variables = {projectId: project_id, name: name, dataType: field_type}
1701
+ variables[:options] = options if options
1702
+
1703
+ result = execute_graphql_query(mutation, **variables)
1704
+ field_data = result.dig("data", "createProjectV2Field", "projectV2Field")
1705
+
1706
+ unless field_data
1707
+ Aidp.log_warn("repository_client", "Failed to create project field", project_id: project_id, name: name)
1708
+ raise "Failed to create project field: #{name}"
1709
+ end
1710
+
1711
+ field = normalize_project_field(field_data)
1712
+ Aidp.log_debug("repository_client", "create_project_field_complete", project_id: project_id, field_id: field[:id])
1713
+ field
1714
+ rescue => e
1715
+ Aidp.log_error("repository_client", "Failed to create project field", project_id: project_id, name: name, error: e.message)
1716
+ raise
1717
+ end
1718
+
1719
+ def create_issue_via_gh(title:, body:, labels: [], assignees: [])
1720
+ Aidp.log_debug("repository_client", "create_issue", title: title, label_count: labels.size, assignee_count: assignees.size)
1721
+
1722
+ cmd = ["gh", "issue", "create", "--repo", full_repo, "--title", title, "--body", body]
1723
+ labels.each { |label| cmd += ["--label", label] }
1724
+ assignees.each { |assignee| cmd += ["--assignee", assignee] }
1725
+
1726
+ stdout, stderr, status = Open3.capture3(*cmd)
1727
+ raise "Failed to create issue via gh: #{stderr.strip}" unless status.success?
1728
+
1729
+ # Parse the issue URL to get the number
1730
+ issue_url = stdout.strip
1731
+ issue_number = issue_url.split("/").last.to_i
1732
+
1733
+ Aidp.log_debug("repository_client", "create_issue_complete", issue_number: issue_number, url: issue_url)
1734
+ {number: issue_number, url: issue_url}
1735
+ rescue => e
1736
+ Aidp.log_error("repository_client", "Failed to create issue", title: title, error: e.message)
1737
+ raise
1738
+ end
1739
+
1740
+ def merge_pull_request_via_gh(number, merge_method: "squash")
1741
+ Aidp.log_debug("repository_client", "merge_pull_request", number: number, merge_method: merge_method)
1742
+
1743
+ cmd = ["gh", "pr", "merge", number.to_s, "--repo", full_repo]
1744
+ case merge_method
1745
+ when "merge"
1746
+ cmd << "--merge"
1747
+ when "squash"
1748
+ cmd << "--squash"
1749
+ when "rebase"
1750
+ cmd << "--rebase"
1751
+ else
1752
+ raise "Unknown merge method: #{merge_method}"
1753
+ end
1754
+
1755
+ # Add auto-delete branch flag
1756
+ cmd << "--delete-branch"
1757
+
1758
+ stdout, stderr, status = Open3.capture3(*cmd)
1759
+ raise "Failed to merge PR via gh: #{stderr.strip}" unless status.success?
1760
+
1761
+ Aidp.log_debug("repository_client", "merge_pull_request_complete", number: number)
1762
+ stdout.strip
1763
+ rescue => e
1764
+ Aidp.log_error("repository_client", "Failed to merge PR", number: number, error: e.message)
1765
+ raise
1766
+ end
1767
+
1768
+ def execute_graphql_query(query, **variables)
1769
+ cmd = ["gh", "api", "graphql", "-f", "query=#{query}"]
1770
+ variables.each do |key, value|
1771
+ flag = value.is_a?(Integer) ? "-F" : "-f"
1772
+ cmd += [flag, "#{key}=#{value}"]
1773
+ end
1774
+
1775
+ stdout, stderr, status = Open3.capture3(*cmd)
1776
+ unless status.success?
1777
+ Aidp.log_warn("repository_client", "GraphQL query failed", error: stderr.strip)
1778
+ raise "GraphQL query failed: #{stderr.strip}"
1779
+ end
1780
+
1781
+ JSON.parse(stdout)
1782
+ rescue JSON::ParserError => e
1783
+ Aidp.log_error("repository_client", "Failed to parse GraphQL response", error: e.message)
1784
+ raise "Failed to parse GraphQL response: #{e.message}"
1785
+ end
1786
+
1787
+ def normalize_project(raw)
1788
+ {
1789
+ id: raw["id"],
1790
+ title: raw["title"],
1791
+ number: raw["number"],
1792
+ url: raw["url"],
1793
+ fields: Array(raw.dig("fields", "nodes")).map { |field| normalize_project_field(field) }
1794
+ }
1795
+ end
1796
+
1797
+ def normalize_project_field(raw)
1798
+ field = {
1799
+ id: raw["id"],
1800
+ name: raw["name"],
1801
+ data_type: raw["dataType"]
1802
+ }
1803
+
1804
+ # Add options for single select fields
1805
+ if raw["options"]
1806
+ field[:options] = raw["options"].map { |opt| {id: opt["id"], name: opt["name"]} }
1807
+ end
1808
+
1809
+ field
1810
+ end
1811
+
1812
+ def normalize_project_item(raw)
1813
+ item = {
1814
+ id: raw["id"],
1815
+ type: raw["type"]
1816
+ }
1817
+
1818
+ # Add content (issue or PR)
1819
+ if raw["content"]
1820
+ content = raw["content"]
1821
+ item[:content] = {
1822
+ number: content["number"],
1823
+ title: content["title"],
1824
+ state: content["state"],
1825
+ url: content["url"]
1826
+ }
1827
+ end
1828
+
1829
+ # Add field values
1830
+ if raw["fieldValues"]
1831
+ field_values = {}
1832
+ Array(raw.dig("fieldValues", "nodes")).each do |fv|
1833
+ next unless fv["field"]
1834
+
1835
+ field_name = fv.dig("field", "name")
1836
+ field_value = fv["text"] || fv["name"]
1837
+ field_values[field_name] = field_value
1838
+ end
1839
+ item[:field_values] = field_values
1840
+ end
1841
+
1842
+ item
1843
+ end
1036
1844
  end
1037
1845
  end
1038
1846
  end