Systematically retrieve and address PR code review comments using make pr-comments. Use when handling code review feedback or addressing PR comments.
Systematically retrieve, categorize, and address all PR code review comments while maintaining quality standards and PR readiness.
Success Criteria:
PR_COMMENT_EVIDENCE records SNAPSHOT_STARTED_AT=<auto-captured ISO time>, SNAPSHOT_CAPTURED_BY=code-review-skill, PR_HEAD=<sha>, plus every review-thread, top-level PR issue, and review body comment from that snapshot as COMMENT_META|url|updatedAt|body_sha256 and COMMENT|url|commit|sha, COMMENT|url|reply|url, or COMMENT|url|decline|url; reply/decline evidence comments must be posted by the PR author or a login in PR_COMMENT_TRUSTED_EVIDENCE_ACTORS and include structured EVIDENCE_SOURCE, EVIDENCE_ACTION, and decline EVIDENCE_REASON; non-evidence comments created or edited after the snapshot block completion until the snapshot and evidence are restarted, except an otherwise qualifying approval review on pushed HEAD whose body is empty or exactly FINAL_APPROVAL_NO_ACTION: truemake ci shows "✅ CI checks successfully passed!"make ai-review-loop runs after make ci and reports PASS on the same commit; if it applies fixes, repeat make ci and make ai-review-loop until both pass without new changesHEAD is pushed and matches gh pr view <number> --repo "$PR_REPO" --json headRefOidHEAD is queried from the base PR repository, is non-empty, non-required contexts have only allowed terminal states (SUCCESS, SKIPPED, or NEUTRAL), every trusted check name/type/source in the trusted base .github/required-pr-checks.txt or externally validated bootstrap manifest and every live base-branch protection required status/check is present with state SUCCESS on that pushed head, source-less branch-protection contexts resolve through trusted manifest type/source mappings, and GitHub Actions check URLs point at the base PR repositorygh pr view <number> --repo "$PR_REPO" --json state,mergeStateStatus,mergeable,reviewDecision,isDraft,reviewRequests shows the PR is open, not draft, not conflicting, reviewDecision is APPROVED, reviewRequests is empty, and a direct review query shows an APPROVED review on the pushed HEAD submitted after the latest addressed comment evidence by a non-author reviewer with OWNER, MEMBER, or COLLABORATOR associationAI Review Loop → PR Comments → Categorize → Apply by Priority → Verify → Run CI → Final AI Review Loop → Push → GitHub Readiness → Done
set -euo pipefail
command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; }
command -v rg >/dev/null || { echo "rg is required" >&2; exit 1; }
command -v unzip >/dev/null || { echo "unzip is required" >&2; exit 1; }
command -v sha256sum >/dev/null || { echo "sha256sum is required" >&2; exit 1; }
command -v base64 >/dev/null || { echo "base64 is required" >&2; exit 1; }
GATE_DEFINITION_CORE_FILE_PATTERN='^(AGENTS\.md|Makefile|\.claude/skills/.*|\.agents/skills/.*|deptrac\.yaml|composer\.(json|lock)|phpunit.*\.xml\.dist|psalm\.xml(\.dist)?|phpstan\.(neon|neon\.dist)|phpmd.*\.xml(\.dist)?|phpinsights.*\.php|infection\.(json|json5)(\.dist)?|phpcs\.xml(\.dist)?|\.php-cs-fixer\.dist\.php|docker-compose.*\.ya?ml|Dockerfile.*|scripts/.*|\.github/workflows/.*|\.github/actions/.*|\.github/linters/.*|\.github/required-pr-checks\.txt|\.prettierrc(\..*)?|\.prettierignore|(.*/)?prettier\.config\.(js|mjs|cjs|ts|json|ya?ml)|(.*/)?\.markdownlint(\..*)?|(.*/)?markdownlint\.config\.(js|mjs|cjs|json|ya?ml)|(.*/)?\.yamllint(\..*)?|(\.codecov|codecov)\.ya?ml|\.coderabbit\.ya?ml|\.qlty/.*|(\.qlty|qlty)\.toml|\.snyk|(\.spectral|spectral)\.ya?ml|(.*/)?spectral\.config\.(js|mjs|cjs|ts|json|ya?ml)|(.*/)?eslint\.config\.(js|mjs|cjs|ts)|(.*/)?\.eslintrc(\..*)?|(.*/)?package(-lock)?\.json|(.*/)?pnpm-lock\.yaml|(.*/)?yarn\.lock|(.*/)?\.graphqlrc(\..*)?|(.*/)?graphql\.config\.(js|mjs|cjs|ts|json|ya?ml)|(.*/)?graphql-inspector(\.config)?\.(js|mjs|cjs|ts|json|ya?ml)|(.*/)?(\.openapi-diff|openapi-diff)(\.config)?\.(js|mjs|cjs|ts|json|ya?ml))$'
GATE_DEFINITION_VALIDATION_SUPPORT_FILE_PATTERN='^(docs/onboarding\.md|tests/CLI/bats/make_ai_review_loop_tests\.bats|specs/autonomous/[0-9]{8}-[0-9]{6}-(ai-review-loop|bmad-fr-nfr-review-gate|bmad-fr-nfr-reviewer-system-design-patterns|ci-gate|gate-definition|required-pr-checks-gate|review-loop)/(architecture|epics|implementation-readiness|manual-evidence|prd|product-brief-distillate|product-brief|research|run-summary|skill-sweep)\.md)$'
GATE_DEFINITION_FILE_PATTERN="(${GATE_DEFINITION_CORE_FILE_PATTERN}|${GATE_DEFINITION_VALIDATION_SUPPORT_FILE_PATTERN})"
GATE_DEFINITION_CHANGES_PRESENT=false
GATE_DEFINITION_EXTERNAL_VALIDATION_PASSED=false
PR_COMMENT_TIMELINE=''
VALIDATED_COMMENT_EVIDENCE_URLS=''
capture_review_comment_snapshot() {
REVIEW_COMMENT_SNAPSHOT_STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
export REVIEW_COMMENT_SNAPSHOT_STARTED_AT
PR_COMMENT_TIMELINE=''
}
assert_review_comment_snapshot_valid() {
: "${REVIEW_COMMENT_SNAPSHOT_STARTED_AT:?Capture REVIEW_COMMENT_SNAPSHOT_STARTED_AT with capture_review_comment_snapshot before addressing PR comments}"
printf '%s\n' "$REVIEW_COMMENT_SNAPSHOT_STARTED_AT" | rg -q '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$'
: "${PR_COMMENT_EVIDENCE:?Set PR_COMMENT_EVIDENCE to the review-comment evidence ledger}"
test -r "$PR_COMMENT_EVIDENCE"
rg -q "^SNAPSHOT_STARTED_AT=$REVIEW_COMMENT_SNAPSHOT_STARTED_AT$" "$PR_COMMENT_EVIDENCE"
rg -q "^SNAPSHOT_CAPTURED_BY=code-review-skill$" "$PR_COMMENT_EVIDENCE"
}
iso_time_epoch() {
date -u -d "$1" +%s
}
body_hash_from_b64() {
printf '%s' "$1" | base64 --decode | sha256sum | awk '{ print $1 }'
}
review_thread_comment_items() {
local owner="$1" repo="$2" comparison="$3" query thread_query page_json page_items has_next cursor thread_refs thread_id thread_cursor thread_has_next thread_json
query='query($owner:String!,$repo:String!,$pr:Int!,$cursor:String){repository(owner:$owner,name:$repo){pullRequest(number:$pr){reviewThreads(first:100,after:$cursor){pageInfo{hasNextPage endCursor} nodes{id comments(first:100){pageInfo{hasNextPage endCursor} nodes{url createdAt updatedAt bodyText}}}}}}}'
cursor=''
while :; do
if [ -n "$cursor" ]; then
page_json="$(gh api graphql -f owner="$owner" -f repo="$repo" -F pr="$PR" -f cursor="$cursor" -f query="$query")"
else
page_json="$(gh api graphql -f owner="$owner" -f repo="$repo" -F pr="$PR" -f query="$query")"
fi
page_items="$(printf '%s\n' "$page_json" | jq -r --arg snapshot "$REVIEW_COMMENT_SNAPSHOT_STARTED_AT" --arg comparison "$comparison" 'def effectiveAt: if ((.updatedAt // .createdAt) > .createdAt) then (.updatedAt // .createdAt) else .createdAt end; .data.repository.pullRequest.reviewThreads.nodes[].comments.nodes[] | select((if $comparison == "before" then effectiveAt <= $snapshot elif $comparison == "after" then effectiveAt > $snapshot else true end) and ((.bodyText // "") | length > 0)) | "\(.url)|\(.createdAt)|\(.updatedAt)|\((.bodyText // "") | @base64)"')"
printf '%s\n' "$page_items"
thread_refs="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.reviewThreads.nodes[] | select(.comments.pageInfo.hasNextPage == true) | "\(.id)|\(.comments.pageInfo.endCursor)"')"
while IFS='|' read -r thread_id thread_cursor; do
[ -n "$thread_id" ] || continue
thread_query='query($threadId:ID!,$cursor:String){node(id:$threadId){... on PullRequestReviewThread{comments(first:100,after:$cursor){pageInfo{hasNextPage endCursor} nodes{url createdAt updatedAt bodyText}}}}}'
while :; do
thread_json="$(gh api graphql -f threadId="$thread_id" -f cursor="$thread_cursor" -f query="$thread_query")"
page_items="$(printf '%s\n' "$thread_json" | jq -r --arg snapshot "$REVIEW_COMMENT_SNAPSHOT_STARTED_AT" --arg comparison "$comparison" 'def effectiveAt: if ((.updatedAt // .createdAt) > .createdAt) then (.updatedAt // .createdAt) else .createdAt end; .data.node.comments.nodes[] | select((if $comparison == "before" then effectiveAt <= $snapshot elif $comparison == "after" then effectiveAt > $snapshot else true end) and ((.bodyText // "") | length > 0)) | "\(.url)|\(.createdAt)|\(.updatedAt)|\((.bodyText // "") | @base64)"')"
printf '%s\n' "$page_items"
thread_has_next="$(printf '%s\n' "$thread_json" | jq -r '.data.node.comments.pageInfo.hasNextPage')"
[ "$thread_has_next" = "true" ] || break
thread_cursor="$(printf '%s\n' "$thread_json" | jq -r '.data.node.comments.pageInfo.endCursor')"
test -n "$thread_cursor" && test "$thread_cursor" != "null"
done
done <<< "$thread_refs"
has_next="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')"
[ "$has_next" = "true" ] || break
cursor="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')"
test -n "$cursor" && test "$cursor" != "null"
done
}
load_pr_comment_timeline() {
local owner repo query page_json page_items has_next cursor
assert_review_comment_snapshot_valid
[ -n "$PR_COMMENT_TIMELINE" ] && return 0
owner="${PR_REPO%%/*}"
repo="${PR_REPO#*/}"
PR_COMMENT_TIMELINE=''
query='query($owner:String!,$repo:String!,$pr:Int!,$cursor:String){repository(owner:$owner,name:$repo){pullRequest(number:$pr){comments(first:100,after:$cursor){pageInfo{hasNextPage endCursor} nodes{url createdAt updatedAt bodyText}}}}}'
cursor=''
while :; do
if [ -n "$cursor" ]; then
page_json="$(gh api graphql -f owner="$owner" -f repo="$repo" -F pr="$PR" -f cursor="$cursor" -f query="$query")"
else
page_json="$(gh api graphql -f owner="$owner" -f repo="$repo" -F pr="$PR" -f query="$query")"
fi
page_items="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.comments.nodes[] | select((.bodyText // "") | length > 0) | "\(.url)|\(.createdAt)|\(.updatedAt)|\((.bodyText // "") | @base64)"')"
PR_COMMENT_TIMELINE="${PR_COMMENT_TIMELINE}"$'\n'"${page_items}"
has_next="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.comments.pageInfo.hasNextPage')"
[ "$has_next" = "true" ] || break
cursor="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.comments.pageInfo.endCursor')"
test -n "$cursor" && test "$cursor" != "null"
done
query='query($owner:String!,$repo:String!,$pr:Int!,$cursor:String){repository(owner:$owner,name:$repo){pullRequest(number:$pr){reviews(first:100,after:$cursor){pageInfo{hasNextPage endCursor} nodes{url createdAt updatedAt bodyText}}}}}'
cursor=''
while :; do
if [ -n "$cursor" ]; then
page_json="$(gh api graphql -f owner="$owner" -f repo="$repo" -F pr="$PR" -f cursor="$cursor" -f query="$query")"
else
page_json="$(gh api graphql -f owner="$owner" -f repo="$repo" -F pr="$PR" -f query="$query")"
fi
page_items="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.reviews.nodes[] | select((.bodyText // "") | length > 0) | "\(.url)|\(.createdAt)|\(.updatedAt)|\((.bodyText // "") | @base64)"')"
PR_COMMENT_TIMELINE="${PR_COMMENT_TIMELINE}"$'\n'"${page_items}"
has_next="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.reviews.pageInfo.hasNextPage')"
[ "$has_next" = "true" ] || break
cursor="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.reviews.pageInfo.endCursor')"
test -n "$cursor" && test "$cursor" != "null"
done
PR_COMMENT_TIMELINE="${PR_COMMENT_TIMELINE}"$'\n'"$(review_thread_comment_items "$owner" "$repo" all)"
PR_COMMENT_TIMELINE="$(printf '%s\n' "$PR_COMMENT_TIMELINE" | sed '/^[[:space:]]*$/d' | sort -u)"
}
comment_time_for_url() {
load_pr_comment_timeline
printf '%s\n' "$PR_COMMENT_TIMELINE" | awk -F'|' -v url="$1" '$1 == url { print ($3 > $2 ? $3 : $2); exit }'
}
assert_pr_commit_evidence() {
local source_url="$1" source_time="$2" commit="$3" commit_time
git rev-parse --verify "${commit}^{commit}" >/dev/null
git merge-base --is-ancestor "$commit" "$LOCAL_HEAD"
! git merge-base --is-ancestor "$commit" "$TRUSTED_BASE_REF"
commit_time="$(git show -s --format=%cI "$commit")"
test "$(iso_time_epoch "$commit_time")" -gt "$(iso_time_epoch "$source_time")"
git log -1 --format=%B "$commit" | rg -Fq "$source_url"
}
comment_json_for_url() {
local url="$1" comment_id
case "$url" in
*"#issuecomment-"*)
comment_id="${url##*#issuecomment-}"
gh api "repos/$PR_REPO/issues/comments/$comment_id"
;;
*"#discussion_r"*)
comment_id="${url##*#discussion_r}"
gh api "repos/$PR_REPO/pulls/comments/$comment_id"
;;
*)
return 1
;;
esac
}
comment_body_for_url() {
comment_json_for_url "$1" | jq -r .body
}
comment_author_for_url() {
comment_json_for_url "$1" | jq -r .user.login
}
assert_evidence_comment_actor() {
local reply_url="$1" pr_author reply_author trusted_actors
pr_author="$(gh pr view "$PR" --repo "$PR_REPO" --json author --jq .author.login)"
reply_author="$(comment_author_for_url "$reply_url")"
if [ "$reply_author" = "$pr_author" ]; then
return 0
fi
: "${PR_COMMENT_TRUSTED_EVIDENCE_ACTORS:?Set comma-separated trusted bot/user logins allowed to post review evidence comments}"
trusted_actors=",$PR_COMMENT_TRUSTED_EVIDENCE_ACTORS,"
printf '%s\n' "$trusted_actors" | rg -Fq ",$reply_author,"
}
assert_pr_reply_evidence() {
local source_url="$1" source_time="$2" reply_url="$3" reply_time reply_body
case "$reply_url" in
"https://github.com/${PR_REPO}/pull/${PR}"#*) ;;
*) return 1 ;;
esac
[ "$reply_url" != "$source_url" ]
reply_time="$(comment_time_for_url "$reply_url")"
test -n "$source_time" && test -n "$reply_time"
test "$(iso_time_epoch "$reply_time")" -gt "$(iso_time_epoch "$source_time")"
assert_evidence_comment_actor "$reply_url"
reply_body="$(comment_body_for_url "$reply_url")"
printf '%s\n' "$reply_body" | rg -Fq "EVIDENCE_SOURCE: $source_url"
printf '%s\n' "$reply_body" | rg -iq '^EVIDENCE_ACTION:[[:space:]]*(addressed|resolved|fixed|implemented|updated|changed|applied)[[:space:]]*$'
}
assert_pr_decline_evidence() {
local source_url="$1" source_time="$2" reply_url="$3" reply_time reply_body
case "$reply_url" in
"https://github.com/${PR_REPO}/pull/${PR}"#*) ;;
*) return 1 ;;
esac
[ "$reply_url" != "$source_url" ]
reply_time="$(comment_time_for_url "$reply_url")"
test -n "$source_time" && test -n "$reply_time"
test "$(iso_time_epoch "$reply_time")" -gt "$(iso_time_epoch "$source_time")"
assert_evidence_comment_actor "$reply_url"
reply_body="$(comment_body_for_url "$reply_url")"
printf '%s\n' "$reply_body" | rg -Fq "EVIDENCE_SOURCE: $source_url"
printf '%s\n' "$reply_body" | rg -iq '^EVIDENCE_ACTION:[[:space:]]*(declined|stale|duplicate|not applicable|not needed|won.t fix|will not fix)[[:space:]]*$'
printf '%s\n' "$reply_body" | rg -iq '^EVIDENCE_REASON:[[:space:]].+'
}
latest_addressed_evidence_time() {
local entry_type source_url entry_action entry_evidence source_time evidence_time source_epoch evidence_epoch latest
latest=0
[ -r "${PR_COMMENT_EVIDENCE:-}" ] || return 0
while IFS='|' read -r entry_type source_url entry_action entry_evidence; do
[ "$entry_type" = "COMMENT" ] || continue
source_time="$(comment_time_for_url "$source_url")"
if [ -n "$source_time" ]; then
source_epoch="$(iso_time_epoch "$source_time")"
if [ "$source_epoch" -gt "$latest" ]; then
latest="$source_epoch"
fi
fi
evidence_time=''
case "$entry_action" in
commit) evidence_time="$(git show -s --format=%cI "$entry_evidence" 2>/dev/null || true)" ;;
reply|decline) evidence_time="$(comment_time_for_url "$entry_evidence")" ;;
esac
if [ -n "$evidence_time" ]; then
evidence_epoch="$(iso_time_epoch "$evidence_time")"
if [ "$evidence_epoch" -gt "$latest" ]; then
latest="$evidence_epoch"
fi
fi
done < "$PR_COMMENT_EVIDENCE"
printf '%s\n' "$latest"
}
assert_comment_evidence_items() {
local evidence_items="$1" source_url source_time source_updated_at source_effective_time source_body_b64 source_body_hash meta_line meta_type meta_url meta_updated_at meta_body_hash evidence_line entry_type entry_url entry_action entry_evidence missing_count
assert_review_comment_snapshot_valid
: "${PR_COMMENT_EVIDENCE:?Set PR_COMMENT_EVIDENCE to a ledger that records PR_HEAD and COMMENT|url|action|evidence lines}"
test -r "$PR_COMMENT_EVIDENCE"
rg -Fqx "PR_HEAD=$LOCAL_HEAD" "$PR_COMMENT_EVIDENCE"
evidence_items="$(printf '%s\n' "$evidence_items" | sed '/^[[:space:]]*$/d' | sort -u)"
[ -n "$evidence_items" ] || return 0
missing_count=0
while IFS='|' read -r source_url source_time source_updated_at source_body_b64; do
source_effective_time="$source_time"
if [ -n "$source_updated_at" ] && [ "$source_updated_at" \> "$source_effective_time" ]; then
source_effective_time="$source_updated_at"
fi
source_body_hash="$(body_hash_from_b64 "$source_body_b64")"
meta_line="$(awk -F'|' -v url="$source_url" '$1 == "COMMENT_META" && $2 == url { print; found = 1; exit } END { if (!found) exit 1 }' "$PR_COMMENT_EVIDENCE" || true)"
if [ -z "$meta_line" ]; then
printf 'Missing comment metadata evidence: %s\n' "$source_url" >&2
missing_count=$((missing_count + 1))
continue
fi
IFS='|' read -r meta_type meta_url meta_updated_at meta_body_hash <<EOF
$meta_line
EOF
if [ "$meta_type" != "COMMENT_META" ] || [ "$meta_url" != "$source_url" ] || [ "$meta_updated_at" != "$source_updated_at" ] || [ "$meta_body_hash" != "$source_body_hash" ]; then
printf 'Invalid comment metadata evidence: %s\n' "$source_url" >&2
missing_count=$((missing_count + 1))
continue
fi
evidence_line="$(awk -F'|' -v url="$source_url" '$1 == "COMMENT" && $2 == url { print; found = 1; exit } END { if (!found) exit 1 }' "$PR_COMMENT_EVIDENCE" || true)"
if [ -z "$evidence_line" ]; then
printf 'Missing comment evidence: %s\n' "$source_url" >&2
missing_count=$((missing_count + 1))
continue
fi
IFS='|' read -r entry_type entry_url entry_action entry_evidence <<EOF
$evidence_line
EOF
if [ "$entry_type" != "COMMENT" ] || [ "$entry_url" != "$source_url" ] || [ -z "$entry_evidence" ]; then
printf 'Invalid comment evidence: %s\n' "$source_url" >&2
missing_count=$((missing_count + 1))
continue
fi
case "$entry_action" in
commit) assert_pr_commit_evidence "$source_url" "$source_effective_time" "$entry_evidence" || missing_count=$((missing_count + 1)) ;;
reply)
if assert_pr_reply_evidence "$source_url" "$source_effective_time" "$entry_evidence"; then
VALIDATED_COMMENT_EVIDENCE_URLS="${VALIDATED_COMMENT_EVIDENCE_URLS}"$'\n'"${entry_evidence}"
else
missing_count=$((missing_count + 1))
fi
;;
decline)
if assert_pr_decline_evidence "$source_url" "$source_effective_time" "$entry_evidence"; then
VALIDATED_COMMENT_EVIDENCE_URLS="${VALIDATED_COMMENT_EVIDENCE_URLS}"$'\n'"${entry_evidence}"
else
missing_count=$((missing_count + 1))
fi
;;
*) missing_count=$((missing_count + 1)) ;;
esac
done <<< "$evidence_items"
test "$missing_count" -eq 0
}
assert_no_unresolved_comments() {
local owner repo query page_json page_count total_count has_next cursor
owner="${PR_REPO%%/*}"
repo="${PR_REPO#*/}"
query='query($owner:String!,$repo:String!,$pr:Int!,$cursor:String){repository(owner:$owner,name:$repo){pullRequest(number:$pr){reviewThreads(first:100,after:$cursor){pageInfo{hasNextPage endCursor} nodes{isResolved comments(first:1){nodes{id}}}}}}}'
total_count=0
cursor=''
while :; do
if [ -n "$cursor" ]; then
page_json="$(gh api graphql -f owner="$owner" -f repo="$repo" -F pr="$PR" -f cursor="$cursor" -f query="$query")"
else
page_json="$(gh api graphql -f owner="$owner" -f repo="$repo" -F pr="$PR" -f query="$query")"
fi
page_count="$(printf '%s\n' "$page_json" | jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and (.comments.nodes | length > 0))] | length')"
total_count=$((total_count + page_count))
has_next="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')"
[ "$has_next" = "true" ] || break
cursor="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')"
test -n "$cursor" && test "$cursor" != "null"
done
test "$total_count" -eq 0
}
non_thread_comment_items() {
local owner="$1" repo="$2" comparison="$3" query page_json page_items has_next cursor
query='query($owner:String!,$repo:String!,$pr:Int!,$cursor:String){repository(owner:$owner,name:$repo){pullRequest(number:$pr){comments(first:100,after:$cursor){pageInfo{hasNextPage endCursor} nodes{url createdAt updatedAt bodyText}}}}}'
cursor=''
while :; do
if [ -n "$cursor" ]; then
page_json="$(gh api graphql -f owner="$owner" -f repo="$repo" -F pr="$PR" -f cursor="$cursor" -f query="$query")"
else
page_json="$(gh api graphql -f owner="$owner" -f repo="$repo" -F pr="$PR" -f query="$query")"
fi
page_items="$(printf '%s\n' "$page_json" | jq -r --arg snapshot "$REVIEW_COMMENT_SNAPSHOT_STARTED_AT" --arg comparison "$comparison" 'def effectiveAt: if ((.updatedAt // .createdAt) > .createdAt) then (.updatedAt // .createdAt) else .createdAt end; .data.repository.pullRequest.comments.nodes[] | select((if $comparison == "before" then effectiveAt <= $snapshot elif $comparison == "after" then effectiveAt > $snapshot else true end) and ((.bodyText // "") | length > 0)) | "\(.url)|\(.createdAt)|\(.updatedAt)|\((.bodyText // "") | @base64)"')"
printf '%s\n' "$page_items"
has_next="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.comments.pageInfo.hasNextPage')"
[ "$has_next" = "true" ] || break
cursor="$(printf '%s\n' "$page_json" | jq -r '.data.repository.pullRequest.comments.pageInfo.endCursor')"
test -n "$cursor" && test "$cursor" != "null"
done
query='query($owner:String!,$repo:String!,$pr:Int!,$cursor:String){repository(owner:$owner,name:$repo){pullRequest(number:$pr){reviews(first:100,after:$cursor){pageInfo{hasNextPage endCursor} nodes{url createdAt updatedAt bodyText}}}}}'
cursor=''
while :; do
if [ -n "$cursor" ]; then
page_json="$(gh api graphql -f owner="$owner" -f repo="$repo" -F pr="$PR" -f cursor="$cursor" -f query="$query")"
else
page_json="$(gh api graphql -f owner="$owner" -f repo="$repo" -F pr="$PR" -f query="$query")"
fi
page_items="$(printf '%s\n' "$page_json" | jq -r --arg snapshot "$REVIEW_COMMENT_SNAPSHOT_STARTED_AT" --arg comparison "$comparison" 'def effectiveAt: if ((.updatedAt // .createdAt) > .createdAt) then (.updatedAt // .createdAt) else .createdAt end; .data.repository.pullRequest.reviews.nodes[] | select((if $comparison == "before" then effectiveAt <= $snapshot elif $comparison == "after" then effectiveAt > $snapshot else true end) and ((.bodyText // "") | length > 0)) | "\
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer