Skip to content

Commit e850837

Browse files
committed
Merge remote-tracking branch 'origin/main' into jg-codex/flagship-demo-agent-guidance
* origin/main: (647 commits) Consolidate analysis/ into internal/analysis/ (#4159) Docs: point Pro license to purchase site (#4160) docs: paired ShakaPerf A/B methodology for RSC perf regressions (#4137) (#4142) Bound the RSCProvider RSC payload cache with an LRU (#3564) (#4097) Harden PR security preflight follow-ups (#4151) [codex] Trust ShakaCode team for agent inputs (#4154) Default adversarial PR review to current branch (#4155) Require hosted CI for generator PRs (#4149) Make review-fix push confirmation context-sensitive (#4152) Harden PR batch security preflight (#4148) Add bidirectional streaming for async props (pull mode) (#4048) Fix main generator pretend spec isolation (#4147) Revive Pro streaming validation coverage (#4035) Fix ci-local detector JSON parsing (#4061) ci(pro): add Rspack build leg for execjs-compatible-dummy (#4091) (#4106) Add High-Risk Mode to adversarial-pr-review (#4050) (#4115) Fix hydration error reporting for thrown values (#4120) Add batch cancellation / drain protocol to stop in-flight PR batches (#4119) [codex] Document current RSC architecture and version policy (#4103) Clarify PR-batch merge authority and ready gates (#4140) ... # Conflicts: # AGENTS_USER_GUIDE.md
2 parents c168588 + f3bd2f8 commit e850837

1,919 files changed

Lines changed: 249161 additions & 29285 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/address-review/SKILL.md

Lines changed: 878 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Fetch and normalize GitHub PR review data without writing to git or GitHub.
5+
#
6+
# Collapses the repeated `gh api ... | jq` review-fetch blocks (review summaries,
7+
# inline review comments, general issue comments, and the reviewThreads GraphQL
8+
# query) into one read-only call that emits a single normalized JSON document.
9+
# The reviewThreads thread_id/is_resolved metadata is joined onto each inline
10+
# comment by node_id so consumers do not have to redo that join.
11+
#
12+
# Requires: gh (GitHub CLI). Ruby stdlib only.
13+
14+
require "json"
15+
require "open3"
16+
17+
# Pure data shaping (no I/O) plus the gh-backed runner. The pure methods take the
18+
# raw `gh --paginate --slurp` output strings so they can be unit-tested directly.
19+
module FetchPrReviewData
20+
SUMMARY_MARKER = "<!-- address-review-summary -->"
21+
22+
REVIEW_THREADS_QUERY = <<~GRAPHQL
23+
query($owner:String!, $name:String!, $pr:Int!, $endCursor:String) {
24+
repository(owner:$owner, name:$name) {
25+
pullRequest(number:$pr) {
26+
reviewThreads(first:100, after:$endCursor) {
27+
nodes { id isResolved comments(first:100) { nodes { id databaseId } } }
28+
pageInfo { hasNextPage endCursor }
29+
}
30+
}
31+
}
32+
}
33+
GRAPHQL
34+
35+
class Error < StandardError; end
36+
37+
module_function
38+
39+
# gh --paginate --slurp wraps the pages of an array endpoint into an outer
40+
# array (one element per page). Flatten one level to get the items.
41+
def slurped_items(raw)
42+
return [] if raw.nil? || raw.strip.empty?
43+
44+
pages = JSON.parse(raw)
45+
return [] unless pages.is_a?(Array)
46+
47+
pages.flat_map { |page| page.is_a?(Array) ? page : [page] }
48+
end
49+
50+
# GraphQL --paginate --slurp wraps pages into an array of response objects.
51+
def thread_nodes(raw)
52+
return [] if raw.nil? || raw.strip.empty?
53+
54+
pages = JSON.parse(raw)
55+
pages = [pages] unless pages.is_a?(Array)
56+
pages.flat_map do |page|
57+
page.dig("data", "repository", "pullRequest", "reviewThreads", "nodes") || []
58+
end
59+
end
60+
61+
def build_review_threads(nodes)
62+
nodes.map do |node|
63+
{
64+
"thread_id" => node["id"],
65+
"is_resolved" => node["isResolved"] ? true : false,
66+
"comments" => (node.dig("comments", "nodes") || []).map do |comment|
67+
{ "node_id" => comment["id"], "id" => comment["databaseId"] }
68+
end
69+
}
70+
end
71+
end
72+
73+
# Map each review comment node_id to its thread metadata.
74+
def thread_index(review_threads)
75+
index = {}
76+
review_threads.each do |thread|
77+
thread["comments"].each do |comment|
78+
node_id = comment["node_id"]
79+
next if node_id.nil?
80+
81+
index[node_id] = { "thread_id" => thread["thread_id"], "is_resolved" => thread["is_resolved"] }
82+
end
83+
end
84+
index
85+
end
86+
87+
def build_review_summaries(raw_reviews)
88+
raw_reviews.filter_map do |review|
89+
next if review["body"].to_s == ""
90+
91+
{
92+
"id" => review["id"],
93+
"type" => "review_summary",
94+
"body" => review["body"],
95+
"state" => review["state"],
96+
"user" => review.dig("user", "login"),
97+
"created_at" => review["submitted_at"],
98+
"html_url" => review["html_url"]
99+
}
100+
end
101+
end
102+
103+
def build_inline_comments(raw_inline, index)
104+
raw_inline.map do |comment|
105+
meta = index[comment["node_id"]]
106+
{
107+
"id" => comment["id"],
108+
"node_id" => comment["node_id"],
109+
"type" => "review",
110+
"path" => comment["path"],
111+
"body" => comment["body"],
112+
"line" => comment["line"],
113+
"start_line" => comment["start_line"],
114+
"user" => comment.dig("user", "login"),
115+
"in_reply_to_id" => comment["in_reply_to_id"],
116+
"created_at" => comment["created_at"],
117+
"html_url" => comment["html_url"],
118+
"thread_id" => meta && meta["thread_id"],
119+
"is_resolved" => meta ? meta["is_resolved"] : false
120+
}
121+
end
122+
end
123+
124+
def build_issue_comments(raw_issue)
125+
raw_issue.map do |comment|
126+
{
127+
"id" => comment["id"],
128+
"node_id" => comment["node_id"],
129+
"type" => "issue",
130+
"body" => comment["body"],
131+
"user" => comment.dig("user", "login"),
132+
"created_at" => comment["created_at"],
133+
"html_url" => comment["html_url"]
134+
}
135+
end
136+
end
137+
138+
# Most recent issue comment whose body starts with the summary marker.
139+
def compute_cutoff(raw_issue)
140+
raw_issue
141+
.select { |comment| comment["body"].to_s.start_with?(SUMMARY_MARKER) }
142+
.filter_map { |comment| comment["created_at"] }
143+
.reject(&:empty?)
144+
.max
145+
.to_s
146+
end
147+
148+
# Build the normalized result hash from the four raw slurped strings.
149+
def assemble(repo:, pr_number:, issue_raw:, reviews_raw:, inline_raw:, threads_raw:)
150+
raw_issue = slurped_items(issue_raw)
151+
review_threads = build_review_threads(thread_nodes(threads_raw))
152+
index = thread_index(review_threads)
153+
154+
{
155+
"repo" => repo,
156+
"pr" => pr_number.to_i,
157+
"review_cutoff_at" => compute_cutoff(raw_issue),
158+
"review_summaries" => build_review_summaries(slurped_items(reviews_raw)),
159+
"inline_comments" => build_inline_comments(slurped_items(inline_raw), index),
160+
"issue_comments" => build_issue_comments(raw_issue),
161+
"review_threads" => review_threads
162+
}
163+
end
164+
165+
def text_summary(result)
166+
resolved_inline = result["inline_comments"].count { |comment| comment["is_resolved"] }
167+
resolved_threads = result["review_threads"].count { |thread| thread["is_resolved"] }
168+
cutoff = result["review_cutoff_at"].empty? ? "(none)" : result["review_cutoff_at"]
169+
[
170+
"PR review data for #{result['repo']}##{result['pr']}",
171+
"review_cutoff_at: #{cutoff}",
172+
"review_summaries: #{result['review_summaries'].length}",
173+
"inline_comments: #{result['inline_comments'].length} (#{resolved_inline} in resolved threads)",
174+
"issue_comments: #{result['issue_comments'].length}",
175+
"review_threads: #{result['review_threads'].length} (#{resolved_threads} resolved)"
176+
].join("\n")
177+
end
178+
179+
USAGE = <<~USAGE.freeze
180+
Usage: fetch-pr-review-data <pr-number> [options]
181+
182+
Fetch normalized review data for a full-PR scan: review summary bodies, inline
183+
review comments (with reviewThreads thread_id/is_resolved joined by node_id),
184+
general issue comments, the raw review threads, and the latest address-review
185+
summary-comment timestamp (review_cutoff_at).
186+
187+
Arguments:
188+
<pr-number> PR number to fetch (integer).
189+
190+
Options:
191+
--repo REPO GitHub repo in OWNER/REPO form. Defaults to gh repo view.
192+
--json Print JSON (default).
193+
--text Print a human-readable count summary instead of JSON.
194+
--self-check Run a parser check plus a read-only gh smoke check.
195+
-h, --help Show this help.
196+
197+
review_cutoff_at is the created_at of the most recent issue comment whose body
198+
starts with "#{SUMMARY_MARKER}", or "" when none exists. Summary and status
199+
marker comments are still included in issue_comments so consumers can filter
200+
them; computing the cutoff here does not drop them.
201+
202+
Requires: gh (GitHub CLI)
203+
USAGE
204+
205+
# gh-backed CLI: parses args, fetches the four endpoints, and prints the
206+
# assembled result. All subprocess calls use array args (no shell).
207+
class Runner
208+
def run(argv)
209+
opts = parse_args(argv)
210+
if opts[:help]
211+
puts USAGE
212+
return 0
213+
end
214+
return self_check if opts[:self_check]
215+
216+
pr_number = validate_pr!(opts[:pr])
217+
repo = resolve_repo!(opts[:repo])
218+
print_result(fetch(repo, pr_number), opts[:format])
219+
0
220+
rescue Error => e
221+
warn "Error: #{e.message}"
222+
1
223+
end
224+
225+
private
226+
227+
def parse_args(argv)
228+
opts = { format: "json", help: false, self_check: false, pr: nil, repo: nil }
229+
args = argv.dup
230+
until args.empty?
231+
arg = args.shift
232+
case arg
233+
when "--repo" then opts[:repo] = take_value!(args, "--repo")
234+
when "--json" then opts[:format] = "json"
235+
when "--text" then opts[:format] = "text"
236+
when "--self-check" then opts[:self_check] = true
237+
when "-h", "--help" then opts[:help] = true
238+
when "--" then nil
239+
else handle_positional(arg, opts)
240+
end
241+
end
242+
opts
243+
end
244+
245+
def handle_positional(arg, opts)
246+
raise Error, "unknown option: #{arg}" if arg.start_with?("-")
247+
raise Error, "unexpected extra argument: #{arg}" unless opts[:pr].nil?
248+
249+
opts[:pr] = arg
250+
end
251+
252+
def take_value!(args, flag)
253+
raise Error, "#{flag} requires a value" if args.empty?
254+
255+
args.shift
256+
end
257+
258+
def validate_pr!(pr_number)
259+
raise Error, "a positive integer PR number is required\n\n#{USAGE}" unless pr_number.to_s.match?(/\A[1-9]\d*\z/)
260+
261+
pr_number
262+
end
263+
264+
# Strict OWNER/REPO: non-empty owner and name, exactly one slash, no spaces.
265+
REPO_FORMAT = %r{\A[^/\s]+/[^/\s]+\z}
266+
267+
def resolve_repo!(repo)
268+
repo ||= capture!("gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner").strip
269+
raise Error, "--repo must be in OWNER/REPO form (got: '#{repo}')" unless repo.match?(REPO_FORMAT)
270+
271+
repo
272+
end
273+
274+
def fetch(repo, pr_number)
275+
owner, name = repo.split("/", 2)
276+
FetchPrReviewData.assemble(
277+
repo:,
278+
pr_number:,
279+
issue_raw: rest("repos/#{repo}/issues/#{pr_number}/comments"),
280+
reviews_raw: rest("repos/#{repo}/pulls/#{pr_number}/reviews"),
281+
inline_raw: rest("repos/#{repo}/pulls/#{pr_number}/comments"),
282+
threads_raw: capture!(
283+
"gh", "api", "graphql", "--paginate", "--slurp",
284+
"-f", "owner=#{owner}", "-f", "name=#{name}", "-F", "pr=#{pr_number}",
285+
"-f", "query=#{REVIEW_THREADS_QUERY}"
286+
)
287+
)
288+
end
289+
290+
def rest(endpoint)
291+
capture!("gh", "api", "--paginate", "--slurp", endpoint)
292+
end
293+
294+
# Run a command with no shell (array args) and return UTF-8 stdout.
295+
def capture!(*cmd)
296+
out, status = Open3.capture2(*cmd)
297+
raise Error, "command failed: #{cmd.first(3).join(' ')}" unless status.success?
298+
299+
out.force_encoding("UTF-8")
300+
end
301+
302+
def print_result(result, format)
303+
if format == "text"
304+
puts FetchPrReviewData.text_summary(result)
305+
else
306+
puts JSON.pretty_generate(result)
307+
end
308+
end
309+
310+
def self_check
311+
errors = self_check_errors(FetchPrReviewData.assemble(**self_check_fixtures))
312+
unless errors.empty?
313+
warn "self-check failed: #{errors.join('; ')}"
314+
return 1
315+
end
316+
puts "assemble parser: ok"
317+
puts gh_smoke
318+
puts "self-check passed"
319+
0
320+
end
321+
322+
def self_check_fixtures
323+
{
324+
repo: "owner/repo",
325+
pr_number: 1234,
326+
issue_raw: '[[{"id":1,"body":"first","created_at":"2026-01-01T00:00:00Z"},' \
327+
'{"id":2,"body":"<!-- address-review-summary -->\nold","created_at":"2026-01-02T00:00:00Z"}],' \
328+
'[{"id":3,"body":"<!-- address-review-summary -->\nnew","created_at":"2026-01-03T00:00:00Z"}]]',
329+
reviews_raw: '[[{"id":10,"body":"fix it","state":"COMMENTED","submitted_at":"2026-01-04T00:00:00Z"},' \
330+
'{"id":11,"body":"","state":"APPROVED"}]]',
331+
inline_raw: '[[{"id":20,"node_id":"RC_20","path":"a.rb"},{"id":21,"node_id":"RC_21","path":"b.rb"}]]',
332+
threads_raw: '[{"data":{"repository":{"pullRequest":{"reviewThreads":{"nodes":[' \
333+
'{"id":"T_A","isResolved":true,"comments":{"nodes":[{"id":"RC_20","databaseId":20}]}},' \
334+
'{"id":"T_B","isResolved":false,"comments":{"nodes":[{"id":"RC_21","databaseId":21}]}}]}}}}}]'
335+
}
336+
end
337+
338+
def self_check_errors(out)
339+
by_id = out["inline_comments"].to_h { |comment| [comment["id"], comment] }
340+
errors = []
341+
errors << "review_cutoff_at=#{out['review_cutoff_at']}" unless out["review_cutoff_at"] == "2026-01-03T00:00:00Z"
342+
errors << "review_summaries=#{out['review_summaries'].length}" unless out["review_summaries"].length == 1
343+
errors << "issue_comments=#{out['issue_comments'].length}" unless out["issue_comments"].length == 3
344+
errors << "RC_20 thread" unless by_id[20]["thread_id"] == "T_A" && by_id[20]["is_resolved"] == true
345+
errors << "RC_21 resolved" unless by_id[21]["is_resolved"] == false
346+
errors
347+
end
348+
349+
def gh_smoke
350+
return "gh smoke: skipped (gh not installed)" unless system("command -v gh > /dev/null 2>&1")
351+
352+
repo, status = Open3.capture2("gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner")
353+
return "gh smoke: skipped (gh not authenticated)" unless status.success? && !repo.strip.empty?
354+
355+
"gh smoke: ok for #{repo.strip}"
356+
end
357+
end
358+
end
359+
360+
exit(FetchPrReviewData::Runner.new.run(ARGV)) if $PROGRAM_NAME == __FILE__

0 commit comments

Comments
 (0)