Skip to content

feat(pr-risk): advisory PR risk grader + label-only shadow check (reusable) - #111

Open
mattmillerai wants to merge 1 commit into
mainfrom
matt/pr-risk-shadow-label
Open

feat(pr-risk): advisory PR risk grader + label-only shadow check (reusable)#111
mattmillerai wants to merge 1 commit into
mainfrom
matt/pr-risk-shadow-label

Conversation

@mattmillerai

Copy link
Copy Markdown
Contributor

Summary

New reusable workflow pr-risk.yml: grades every PR event into an advisory risk tier R0 (safest) .. R3 (riskiest) and syncs exactly one label — risk:R0..risk:R3, or risk:ungraded when an input could not be read. The label is the entire product: nothing is gated, routed, commented on, or merged. Humans glance at the label and agree or disagree; disagreement is recorded with a human-owned risk-dispute label the grader never touches.

grade = worst(path_floor, provenance, reversibility) — three deterministic axes, worst wins, so no axis can move a PR into a safer lane than another axis put it. The whole path is gh + jq; no model call anywhere.

Details

  • scripts/pr-risk/grade-pr-risk.sh — the grader. Three axes: a versioned path-glob map (worst tier over every matched rule, so a docs file can never cancel a migration), provenance (registered runbook producers must assert identity AND diff shape; fork / first-time contributors are R3 with no exceptions), and reversibility (persistent-state mutation, deletions under sensitive classes, whether green checks covered the lines). Unreadable input grades unknown, never a confident tier.
  • scripts/pr-risk/apply-risk-label.sh — the one write. Owns exactly the five mapped labels: removes stale ones, applies the computed one, touches nothing else. Missing labels are created color-coded on first use. Label text is remappable via label_map.
  • Defaults are generic; consumers sharpen them. .github/risk.json / .github/risk-runbooks.json in the consumer repo are read from the PR's base ref, so a PR cannot edit the rules that judge it (and touching them at all grades R3 by the map's first rule). Absent falls back to the shipped defaults; present-but-invalid fails loudly.
  • The grading job excludes its own run from the check rollup it reads. Its own check is always in-flight at grade time, so the raw rollup could never be green mid-run and every live grade would floor at R2 as a measurement artifact. The job recomputes the rollup from individual contexts minus its own workflow, and re-polls up to wait_for_checks_minutes (default 10) for the rest of CI to settle before labeling.
  • Labels ride the plain GITHUB_TOKEN, which cannot fire labeled triggers — the shadow check is structurally unable to start a workflow cascade. No secrets required.
  • No PR code is ever checked out: logic + defaults load from this repo at the pinned workflows_ref.
  • test-pr-risk.yml: shellcheck + two hermetic suites (35 checks — synthetic records plus a stubbed gh for the live-PR path, including the self-excluding-rollup cases). README catalog row + feature README (scripts/pr-risk/README.md) included.

ELI5

Some pull requests are scary (they touch billing, or database migrations, or the CI itself) and some are boring (a typo fix in a README). Today a human has to open each one to tell the difference. This workflow reads the list of changed files, who made the PR, and whether the tests passed, and puts a colored sticker on it: green for boring, red for scary, gray for "couldn't tell".

That sticker doesn't DO anything — it can't merge, block, or approve. It exists so the team can spend a couple of weeks checking whether the stickers match their own judgement. If someone thinks a sticker is wrong, they slap on a risk-dispute sticker and say why. Only after the stickers prove themselves does anyone talk about acting on them.

Two design points that matter: the rules that decide the grade can't be changed by the PR being graded (they're read from the target branch, not the PR), and the checker leaves out its own still-running check when it asks "did the tests pass?" — otherwise it would always answer "not yet" about itself.

…sable)

Grades every PR event into R0..R3 = worst(path_floor, provenance,
reversibility) and syncs one label; nothing gates, routes, or merges.
Deterministic gh+jq, no LLM. Generic default map/registry here; consumers
sharpen via .github/risk.json read from the PR base ref. The grading job
excludes its own run from the check rollup it reads and waits for the rest
to settle, so live grades are not floored at R2 by the measurement itself.
@mattmillerai mattmillerai self-assigned this Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds deterministic PR risk grading across path, provenance, and reversibility axes. Adds configurable risk labels, base-ref overrides, pending-check handling, advisory summaries, tests, and documentation.

Changes

PR risk grading

Layer / File(s) Summary
Grader engine and risk configuration
scripts/pr-risk/grade-pr-risk.sh, scripts/pr-risk/risk-map.v0.json, scripts/pr-risk/runbook-registry.v0.json
The grader validates configuration, collects GitHub PR data, evaluates path, provenance, and reversibility axes, and returns versioned risk results.
Risk label synchronization
scripts/pr-risk/apply-risk-label.sh
The label script validates mappings, removes stale grader-owned labels, creates missing labels, and applies the computed tier.
Reusable workflow integration
.github/workflows/pr-risk.yml
The reusable workflow loads base-ref overrides, waits for pending checks, runs the grader, applies one risk label, and writes an advisory summary.
Validation and documentation
.github/workflows/test-pr-risk.yml, scripts/pr-risk/tests/*, README.md, scripts/pr-risk/README.md
CI runs shellcheck and hermetic tests. Documentation describes grading, configuration, labels, and workflow behavior.

Sequence Diagram(s)

sequenceDiagram
  participant PullRequest
  participant pr-risk.yml
  participant grade-pr-risk.sh
  participant apply-risk-label.sh
  PullRequest->>pr-risk.yml: Trigger reusable workflow
  pr-risk.yml->>grade-pr-risk.sh: Load configuration and grade PR
  grade-pr-risk.sh-->>pr-risk.yml: Return tier and axis reasons
  pr-risk.yml->>apply-risk-label.sh: Synchronize risk label
  apply-risk-label.sh-->>PullRequest: Update PR labels
Loading
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/pr-risk-shadow-label
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/pr-risk-shadow-label

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/pr-risk.yml:
- Around line 105-111: Update the workflows_ref input in the pr-risk workflow to
be required and remove its floating main default, ensuring callers explicitly
provide the pinned repository reference used to load grader and workflow
scripts.
- Around line 121-126: Add inline documentation to the permissions block in the
workflow job, placing the existing header’s rationale beside each granted scope,
especially issues: write. Document contents, pull-requests, checks, and statuses
as well, without changing their permission levels.
- Around line 190-191: Clamp the wait duration used to compute deadline in the
workflow’s wait-for-checks step to the job’s available timeout, ensuring
wait_for_checks_minutes cannot make the job sleep past timeout-minutes and
preventing the subsequent label step from being skipped.
- Around line 192-207: Update the retry loop around the bash "$TOOL" invocation
so rc=3 is retried with bounded backoff until a defined retry limit or the
existing deadline, labeling the PR ungraded only after retries are exhausted;
retain immediate failure for other unexpected codes. Replace the fixed 30-second
pending poll in this loop with increasing bounded backoff while preserving the
deadline and checks_pending_excl_self behavior.
- Around line 1-57: Add a header comment to the reusable workflow documenting
that callers provide no secrets and that it uses only GITHUB_TOKEN. Create or
move the semver-major v1 Git tag to the commit containing this workflow, without
changing the existing README catalog entry.

In @.github/workflows/test-pr-risk.yml:
- Around line 1-8: Add the required workflow header before name in the Test
pr-risk scripts workflow, documenting its event triggers, no inputs, no secrets,
and no reusable-workflow caller pattern. Preserve the existing workflow name and
content below the new header.

In `@scripts/pr-risk/apply-risk-label.sh`:
- Around line 92-110: URL-encode label names before interpolating them into
GitHub API path segments in the stale-label DELETE and target-label lookup flows
around has and the label-management loop. Apply the encoding to both l and
TARGET while preserving the existing raw names for logging, form fields, and
label comparisons so names containing spaces or slashes produce valid requests.
- Around line 86-87: Update the labels API request assigned to current in the
label-reading flow to include gh’s --paginate option, ensuring all labels are
loaded before has() and the removal loop evaluate them. Preserve the existing
error handling and JSON output format.

In `@scripts/pr-risk/grade-pr-risk.sh`:
- Around line 356-361: Update the PR risk GraphQL query and the parsing logic
around changed_paths_status to paginate the files connection through endCursor
until hasNextPage is false, retaining unknown only when a hard page-count cap is
reached. Aggregate all returned file nodes before grading so large PRs receive
normal path and reversibility scores. Also paginate labels beyond the first 20
and merge all label pages before detecting agent-coded or related provenance
labels.
- Around line 291-292: Move the test-file matching patterns used by the
$touched_test calculation into the versioned risk map, while retaining the
current regex as the fallback when the map key is absent or empty. Add the
matching key to risk-map.v0.json and update the map validation to document and
validate it, including patterns covering Python, Java, and Ruby test naming
conventions.

In `@scripts/pr-risk/risk-map.v0.json`:
- Around line 24-25: Update scripts/pr-risk/risk-map.v0.json at lines 24-25 to
use **/risk-map.*.json and include **/runbook-registry.*.json, **/pr-risk/**,
and **/.github/risk-runbooks.json; update lines 53-54 from *-test.sh to
**/*-test.sh. Add a regression case in
scripts/pr-risk/tests/test_grade_pr_risk.sh that grades a nested
scripts/pr-risk/risk-map.v0.json path and asserts path_floor is R3.
- Around line 53-54: Update the "tests" class path patterns in the risk-map
configuration so the shell-test glob matches files at any directory depth, not
only root-level files. Change the existing "*-test.sh" pattern to its recursive
equivalent while preserving the other test path patterns and classification.

In `@scripts/pr-risk/runbook-registry.v0.json`:
- Around line 31-33: Update the permitted_paths configuration associated with
the deps rule to include every dependency-manifest pattern listed by deps in
risk-map.v0.json, including Gemfile, Gemfile.lock, composer.json, composer.lock,
*.gemspec, poetry.lock, Pipfile, Pipfile.lock, npm-shrinkwrap.json, setup.py,
and go.work*. Prefer deriving this set from the deps rule if the registry
supports it; otherwise extend the existing list while preserving all current
entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0a3cd30f-3f43-4461-b395-8b2f25aa8a0d

📥 Commits

Reviewing files that changed from the base of the PR and between bcde90f and 9ea1a55.

📒 Files selected for processing (10)
  • .github/workflows/pr-risk.yml
  • .github/workflows/test-pr-risk.yml
  • README.md
  • scripts/pr-risk/README.md
  • scripts/pr-risk/apply-risk-label.sh
  • scripts/pr-risk/grade-pr-risk.sh
  • scripts/pr-risk/risk-map.v0.json
  • scripts/pr-risk/runbook-registry.v0.json
  • scripts/pr-risk/tests/test_apply_risk_label.sh
  • scripts/pr-risk/tests/test_grade_pr_risk.sh

Comment on lines +1 to +57
name: PR Risk Grade (reusable)

# Reusable ADVISORY PR risk grader — the shadow-check rung of the PR risk-grading ladder.
# Grades every PR event into a tier R0 (safest) .. R3 (riskiest) and syncs ONE label
# (`risk:R0` .. `risk:R3`, or `risk:ungraded` when an input was unreadable). That label is
# the entire product: nothing is gated, nothing is blocked, nothing merges, no comment is
# posted. Humans look at the label and agree or disagree; disagreement is recorded by adding
# the `risk-dispute` label (which this workflow never touches) plus a comment saying why.
#
# grade = worst(path_floor, provenance, reversibility) — three deterministic axes; the worst
# tier wins, so no axis can move a PR into a safer lane than another axis put it. No LLM, no
# model call anywhere: `gh` + `jq` over the PR's own API record. See
# scripts/pr-risk/grade-pr-risk.sh for the axes and the unknown contract.
#
# The grader and its default risk map load from THIS repo at the pinned `workflows_ref`,
# never from the graded PR's checkout (no PR code is checked out at all) — a PR cannot edit
# the rules that judge it. A consumer repo sharpens the generic defaults by committing
# `.github/risk.json` (map) / `.github/risk-runbooks.json` (runbook registry), which are read
# from the PR's BASE ref: present-but-invalid fails the run loudly; absent falls back to the
# defaults in scripts/pr-risk/.
#
# CHECKS SETTLE BEFORE THE LABEL DOES: the reversibility axis asks "did tests covering these
# lines actually run", and at event time the rest of the rollup is usually still running (the
# grading job itself is excluded from the rollup it reads — see --self-context in the
# grader). The job re-polls until the other checks settle or `wait_for_checks_minutes` runs
# out, then labels what it has. Pair the caller with a per-PR cancel-in-progress concurrency
# group so a new push supersedes a waiting run instead of stacking behind it.
#
# The label is applied with the plain GITHUB_TOKEN on purpose: GITHUB_TOKEN-applied labels
# cannot fire `labeled` triggers, so the shadow check is structurally unable to start a
# workflow cascade. Fork PRs under a plain `pull_request` trigger get a read-only token and
# the label write will fail — enroll public repos with `pull_request_target` instead (safe
# here by construction: this workflow never checks out or executes PR code).
#
# Caller pattern (consumer repo, .github/workflows/ci-pr-risk.yml):
#
# name: CI - PR Risk Grade
# on:
# pull_request:
# types: [opened, synchronize, reopened, ready_for_review]
# concurrency:
# group: pr-risk-${{ github.event.pull_request.number }}
# cancel-in-progress: true
# permissions:
# contents: read
# jobs:
# pr-risk:
# permissions:
# contents: read
# issues: write # the risk label rides the issues API
# pull-requests: read
# checks: read # the check rollup the reversibility axis reads
# statuses: read
# uses: Comfy-Org/github-workflows/.github/workflows/pr-risk.yml@<sha> # v1
# with:
# workflows_ref: <same sha>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the README catalog for a pr-risk.yml entry and confirm existing major-tag practice.
set -uo pipefail
rg -n -C 3 'pr-risk' README.md || echo "NO pr-risk row in README.md"
echo "--- catalog rows for comparison ---"
rg -n -C 2 '\.github/workflows/[a-z0-9-]+\.yml@' README.md | head -60
echo "--- existing tags ---"
git tag --list | head -40

Repository: Comfy-Org/github-workflows

Length of output: 7339


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- workflow contract ---'
sed -n '1,140p' .github/workflows/pr-risk.yml
printf '%s\n' '--- README catalog header and pr-risk row ---'
sed -n '1,28p' README.md
printf '%s\n' '--- versioning and reusable-workflow references ---'
rg -n -i 'major tag|semver|version|@<sha>|workflow.*@|reusable' README.md .github . || true
printf '%s\n' '--- repository refs and workflow declarations ---'
git show-ref --tags || true
rg -n '^\s*(on:|workflow_call:|secrets:|inputs:|workflows_ref:)' .github/workflows/pr-risk.yml

Repository: Comfy-Org/github-workflows

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- README versioning and new-workflow requirements ---'
sed -n '44,72p' README.md
printf '%s\n' '--- pr-risk secret references ---'
rg -n -i 'secret|token|github\.token|secrets\.' .github/workflows/pr-risk.yml
printf '%s\n' '--- workflow header contract markers ---'
sed -n '1,70p' .github/workflows/pr-risk.yml | rg -n -i 'trigger|input|secret|caller|workflow_call|GITHUB_TOKEN'
printf '%s\n' '--- tag refs ---'
git show-ref --tags || true

Repository: Comfy-Org/github-workflows

Length of output: 1931


Document the secret contract and publish the v1 tag.

Add a header line stating that no caller secrets are required and that the workflow uses only GITHUB_TOKEN. Create or move the semver major v1 tag; no tag currently exists. The README catalog entry is already present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-risk.yml around lines 1 - 57, Add a header comment to
the reusable workflow documenting that callers provide no secrets and that it
uses only GITHUB_TOKEN. Create or move the semver-major v1 Git tag to the commit
containing this workflow, without changing the existing README catalog entry.

Source: Coding guidelines

Comment on lines +105 to +111
workflows_ref:
description: >-
Ref of Comfy-Org/github-workflows to load the grader + default map
from. Pin to the same ref you pin `uses:` to.
type: string
required: false
default: main

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

workflows_ref must not default to main.

The header at Lines 15-16 states the grader loads from "THIS repo at the pinned workflows_ref". The default is main, which is a floating ref. A caller who pins uses: to a SHA and omits workflows_ref gets a SHA-pinned workflow that then downloads grader code from HEAD of main. The two halves of the same tool drift apart, and the grading logic becomes mutable after review.

Make the input required, so the caller states the ref, and the run fails loudly when they forget. A floating default is a supply chain with no chain.

As per coding guidelines: "Load reviewer, checker, and other workflow scripts from a pinned reference of this repository at runtime, never from the caller's checkout or a local path."

🔒️ Proposed fix
       workflows_ref:
         description: >-
           Ref of Comfy-Org/github-workflows to load the grader + default map
-          from. Pin to the same ref you pin `uses:` to.
+          from. Pin to the SAME commit SHA you pin `uses:` to. Required: a
+          floating default would let the grader code drift from the pinned
+          workflow that loads it.
         type: string
-        required: false
-        default: main
+        required: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
workflows_ref:
description: >-
Ref of Comfy-Org/github-workflows to load the grader + default map
from. Pin to the same ref you pin `uses:` to.
type: string
required: false
default: main
workflows_ref:
description: >-
Ref of Comfy-Org/github-workflows to load the grader + default map
from. Pin to the SAME commit SHA you pin `uses:` to. Required: a
floating default would let the grader code drift from the pinned
workflow that loads it.
type: string
required: true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-risk.yml around lines 105 - 111, Update the
workflows_ref input in the pr-risk workflow to be required and remove its
floating main default, ensuring callers explicitly provide the pinned repository
reference used to load grader and workflow scripts.

Source: Coding guidelines

Comment on lines +121 to +126
permissions:
contents: read
issues: write
pull-requests: read
checks: read
statuses: read

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the job permissions.

zizmor reports undocumented-permissions at Line 123. Each scope here has a specific reason, and the header already explains them. Move that reasoning next to the grant so a reader auditing issues: write does not have to hunt for it.

♻️ Proposed change
     permissions:
-      contents: read
-      issues: write
-      pull-requests: read
-      checks: read
-      statuses: read
+      contents: read        # read the base-ref risk config via the contents API
+      issues: write         # the one write: sync the risk label (labels ride the issues API)
+      pull-requests: read   # read the PR record the grader grades
+      checks: read          # the check rollup the reversibility axis reads
+      statuses: read        # commit statuses in the same rollup
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
permissions:
contents: read
issues: write
pull-requests: read
checks: read
statuses: read
permissions:
contents: read # read the base-ref risk config via the contents API
issues: write # the one write: sync the risk label (labels ride the issues API)
pull-requests: read # read the PR record the grader grades
checks: read # the check rollup the reversibility axis reads
statuses: read # commit statuses in the same rollup
🧰 Tools
🪛 zizmor (1.28.0)

[warning] 123-123: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment

(undocumented-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-risk.yml around lines 121 - 126, Add inline
documentation to the permissions block in the workflow job, placing the existing
header’s rationale beside each granted scope, especially issues: write. Document
contents, pull-requests, checks, and statuses as well, without changing their
permission levels.

Source: Linters/SAST tools

Comment on lines +190 to +191
deadline=$(( $(date +%s) + 60 * WAIT_MINUTES ))
waited=0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Clamp the wait against the job timeout.

timeout-minutes is 30. The input description asks the caller to keep wait_for_checks_minutes at or below 25, but nothing enforces it. A caller who passes 40 gets a job that is cancelled while still sleeping: the label step never runs, and the PR keeps whatever label the previous push left. The check goes red for a reason no one will guess. Clamp in the workflow instead of trusting the reader.

🐛 Proposed fix
+          # The job times out at 30 minutes; never wait past the point where
+          # the label step can still run.
+          [ "$WAIT_MINUTES" -le 25 ] 2>/dev/null || WAIT_MINUTES=25
           deadline=$(( $(date +%s) + 60 * WAIT_MINUTES ))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
deadline=$(( $(date +%s) + 60 * WAIT_MINUTES ))
waited=0
# The job times out at 30 minutes; never wait past the point where
# the label step can still run.
[ "$WAIT_MINUTES" -le 25 ] 2>/dev/null || WAIT_MINUTES=25
deadline=$(( $(date +%s) + 60 * WAIT_MINUTES ))
waited=0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-risk.yml around lines 190 - 191, Clamp the wait
duration used to compute deadline in the workflow’s wait-for-checks step to the
job’s available timeout, ensuring wait_for_checks_minutes cannot make the job
sleep past timeout-minutes and preventing the subsequent label step from being
skipped.

Comment on lines +192 to +207
while :; do
rc=0
bash "$TOOL" "${ARGS[@]}" > record.json || rc=$?
case "$rc" in
0|1) ;; # graded (1 = graded but unknown — still labeled)
3) echo "PR unreadable via the API — labeling ungraded"
printf '{}' > record.json
break ;;
*) echo "grader failed (rc=$rc)"; exit 1 ;; # setup/usage bug: fail loud
esac
pending="$(jq -r '.checks_pending_excl_self // false' record.json)"
if [ "$pending" = "true" ] && [ "$(date +%s)" -lt "$deadline" ]; then
sleep 30; waited=$(( waited + 30 )); continue
fi
break
done

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Retry an unreadable PR before labeling it ungraded, and reconsider the fixed 30s poll.

Two points on this loop.

First, rc=3 breaks immediately. Exit 3 fires whenever the GraphQL call fails, which includes rate limits, secondary rate limits, and transient 5xx. One blip therefore produces risk:ungraded, and the summary tells the reader the PR could not be read — a durable verdict from a momentary hiccup. The loop already owns a deadline; use it. Retry rc=3 a few times with backoff, and label ungraded only when the retries are exhausted.

Second, the poll is a fixed 30s sleep for up to wait_for_checks_minutes. At the default that is a runner idling up to 10 minutes on every synchronize event, per PR, across the fleet. The grade itself takes one API call. Increasing backoff would cut both runner minutes and API calls with no change to the settle behaviour. Waiting is fine; waiting at full price is not.

♻️ Proposed change: bounded retry on rc=3 plus backoff
           deadline=$(( $(date +%s) + 60 * WAIT_MINUTES ))
           waited=0
+          unreadable=0
+          nap=15
           while :; do
             rc=0
             bash "$TOOL" "${ARGS[@]}" > record.json || rc=$?
             case "$rc" in
               0|1) ;;                     # graded (1 = graded but unknown — still labeled)
-              3)  echo "PR unreadable via the API — labeling ungraded"
+              3)  unreadable=$(( unreadable + 1 ))
+                  if [ "$unreadable" -lt 3 ] && [ "$(date +%s)" -lt "$deadline" ]; then
+                    echo "PR read failed (attempt $unreadable) — retrying in ${nap}s"
+                    sleep "$nap"; waited=$(( waited + nap )); nap=$(( nap * 2 )); continue
+                  fi
+                  echo "PR unreadable via the API after $unreadable attempts — labeling ungraded"
                   printf '{}' > record.json
                   break ;;
               *)  echo "grader failed (rc=$rc)"; exit 1 ;;   # setup/usage bug: fail loud
             esac
             pending="$(jq -r '.checks_pending_excl_self // false' record.json)"
             if [ "$pending" = "true" ] && [ "$(date +%s)" -lt "$deadline" ]; then
-              sleep 30; waited=$(( waited + 30 )); continue
+              sleep "$nap"; waited=$(( waited + nap ))
+              [ "$nap" -lt 120 ] && nap=$(( nap * 2 ))
+              continue
             fi
             break
           done
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while :; do
rc=0
bash "$TOOL" "${ARGS[@]}" > record.json || rc=$?
case "$rc" in
0|1) ;; # graded (1 = graded but unknown — still labeled)
3) echo "PR unreadable via the API — labeling ungraded"
printf '{}' > record.json
break ;;
*) echo "grader failed (rc=$rc)"; exit 1 ;; # setup/usage bug: fail loud
esac
pending="$(jq -r '.checks_pending_excl_self // false' record.json)"
if [ "$pending" = "true" ] && [ "$(date +%s)" -lt "$deadline" ]; then
sleep 30; waited=$(( waited + 30 )); continue
fi
break
done
deadline=$(( $(date +%s) + 60 * WAIT_MINUTES ))
waited=0
unreadable=0
nap=15
while :; do
rc=0
bash "$TOOL" "${ARGS[@]}" > record.json || rc=$?
case "$rc" in
0|1) ;; # graded (1 = graded but unknown — still labeled)
3) unreadable=$(( unreadable + 1 ))
if [ "$unreadable" -lt 3 ] && [ "$(date +%s)" -lt "$deadline" ]; then
echo "PR read failed (attempt $unreadable) — retrying in ${nap}s"
sleep "$nap"; waited=$(( waited + nap )); nap=$(( nap * 2 )); continue
fi
echo "PR unreadable via the API after $unreadable attempts — labeling ungraded"
printf '{}' > record.json
break ;;
*) echo "grader failed (rc=$rc)"; exit 1 ;; # setup/usage bug: fail loud
esac
pending="$(jq -r '.checks_pending_excl_self // false' record.json)"
if [ "$pending" = "true" ] && [ "$(date +%s)" -lt "$deadline" ]; then
sleep "$nap"; waited=$(( waited + nap ))
[ "$nap" -lt 120 ] && nap=$(( nap * 2 ))
continue
fi
break
done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-risk.yml around lines 192 - 207, Update the retry loop
around the bash "$TOOL" invocation so rc=3 is retried with bounded backoff until
a defined retry limit or the existing deadline, labeling the PR ungraded only
after retries are exhausted; retain immediate failure for other unexpected
codes. Replace the fixed 30-second pending poll in this loop with increasing
bounded backoff while preserving the deadline and checks_pending_excl_self
behavior.

Comment on lines +291 to +292
| ($plist | any(matches_any($M.flippable_flag_paths // []))) as $flag
| ($plist | any(test("(_test\\.|\\.test\\.|\\.spec\\.|(^|/)tests?/|-test\\.sh$)"))) as $touched_test

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the test-file patterns into the versioned map.

Line 292 hardcodes the "did a test file change" heuristic. Every other input on this axis comes from the map. The regex misses test_*.py, *_test.py, *Test.java, and *_spec.rb, so a Python or Java consumer can never reach clean_tier and sits at R1 forever. A consumer cannot fix that with .github/risk.json, which is the one lever the workflow gives them. Put the patterns in the map, and keep the current regex as the fallback — a map-shaped test beats a test that is map-shaped in name only.

♻️ Proposed change: read glob patterns from the map
-         | ($plist | any(test("(_test\\.|\\.test\\.|\\.spec\\.|(^|/)tests?/|-test\\.sh$)"))) as $touched_test
+         | (($M.test_path_globs // []) as $tg
+            | if ($tg | length) > 0 then ($plist | any(matches_any($tg)))
+              else ($plist | any(test("(_test\\.|\\.test\\.|\\.spec\\.|(^|/)tests?/|-test\\.sh$)"))) end) as $touched_test

Add the matching key to scripts/pr-risk/risk-map.v0.json and document it in the map validation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| ($plist | any(matches_any($M.flippable_flag_paths // []))) as $flag
| ($plist | any(test("(_test\\.|\\.test\\.|\\.spec\\.|(^|/)tests?/|-test\\.sh$)"))) as $touched_test
| ($plist | any(matches_any($M.flippable_flag_paths // []))) as $flag
| (($M.test_path_globs // []) as $tg
| if ($tg | length) > 0 then ($plist | any(matches_any($tg)))
else ($plist | any(test("(_test\\.|\\.test\\.|\\.spec\\.|(^|/)tests?/|-test\\.sh$)"))) end) as $touched_test
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-risk/grade-pr-risk.sh` around lines 291 - 292, Move the test-file
matching patterns used by the $touched_test calculation into the versioned risk
map, while retaining the current regex as the fallback when the map key is
absent or empty. Add the matching key to risk-map.v0.json and update the map
validation to document and validate it, including patterns covering Python,
Java, and Ruby test naming conventions.

Comment on lines +356 to +361
labels(first:20){ nodes{ name } }
commits(last:1){ nodes{ commit{ statusCheckRollup{ state
contexts(first:100){ pageInfo{ hasNextPage } nodes{ __typename
... on CheckRun{ name status conclusion checkSuite{ workflowRun{ workflow{ name } } } }
... on StatusContext{ context state } } } } } } }
files(first:100){ pageInfo{ hasNextPage } nodes{ path additions deletions changeType } }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Paginate the file list instead of grading every large PR ungraded.

files(first:100) caps the changed-path list. Above 100 files the record sets changed_paths_status: "unknown", which nulls the path floor AND the reversibility axis, so the whole grade is unknown. The PRs that touch 150 files are the PRs a risk grade helps most, and right now they all land in the ungraded lane. Page the files connection with endCursor until hasNextPage is false, and keep the unknown result only for a hard page-count cap.

Related: labels(first:20) at Line 356 has no truncation twin, unlike files and contexts. A PR with more than 20 labels can silently lose agent-coded. Both default provenance tiers are R1 today, so no tier moves — but a consumer map that splits agent-supervised from human would grade off a label list nobody confirmed was complete.

Also applies to: 400-403

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-risk/grade-pr-risk.sh` around lines 356 - 361, Update the PR risk
GraphQL query and the parsing logic around changed_paths_status to paginate the
files connection through endCursor until hasNextPage is false, retaining unknown
only when a hard page-count cap is reached. Aggregate all returned file nodes
before grading so large PRs receive normal path and reversibility scores. Also
paginate labels beyond the first 20 and merge all label pages before detecting
agent-coded or related provenance labels.

Comment on lines +24 to +25
{ "class": "risk-map", "tier": "R3", "why": "touching the map itself is automatically R3 — a PR must not be able to lower its own grade",
"paths": ["risk-map.*.json", ".github/risk.json", ".github/risk-runbooks.json", "**/.github/risk.json"] },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Root-anchored globs in the default map. glob2re in scripts/pr-risk/grade-pr-risk.sh (Lines 166-170) anchors the whole path with ^/$, and * does not cross /. Any glob without a leading **/ therefore matches root-level files only. Two rules in this map assume otherwise, so they silently match nothing in a real tree — a floor that never touches the ground.

  • scripts/pr-risk/risk-map.v0.json#L24-L25: prefix risk-map.*.json with **/, and add **/runbook-registry.*.json, **/pr-risk/**, and **/.github/risk-runbooks.json. Without this, scripts/pr-risk/risk-map.v0.json and the grader scripts grade at default_tier R0, and a PR that edits the judge is graded safest by that judge.
  • scripts/pr-risk/risk-map.v0.json#L53-L54: change *-test.sh to **/*-test.sh, so shell tests outside a tests/ directory match the R0 tests class.

Then add a regression case to scripts/pr-risk/tests/test_grade_pr_risk.sh that feeds a nested scripts/pr-risk/risk-map.v0.json path and asserts path_floor is R3.

📍 Affects 1 file
  • scripts/pr-risk/risk-map.v0.json#L24-L25 (this comment)
  • scripts/pr-risk/risk-map.v0.json#L53-L54
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-risk/risk-map.v0.json` around lines 24 - 25, Update
scripts/pr-risk/risk-map.v0.json at lines 24-25 to use **/risk-map.*.json and
include **/runbook-registry.*.json, **/pr-risk/**, and
**/.github/risk-runbooks.json; update lines 53-54 from *-test.sh to
**/*-test.sh. Add a regression case in
scripts/pr-risk/tests/test_grade_pr_risk.sh that grades a nested
scripts/pr-risk/risk-map.v0.json path and asserts path_floor is R3.

Comment on lines +53 to +54
{ "class": "tests", "tier": "R0", "why": "test-only changes cannot break production behaviour",
"paths": ["**/*_test.go", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/test/**", "**/tests/**", "*-test.sh"] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

*-test.sh matches root-level files only.

* does not cross /, so this glob compiles to ^[^/]*-test\.sh$. scripts/pr-risk/tests/test_apply_risk_label.sh is rescued by **/tests/**, but a shell test outside a tests/ directory is not. Prefix the glob with **/.

🐛 Proposed fix
-      "paths": ["**/*_test.go", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/test/**", "**/tests/**", "*-test.sh"] }
+      "paths": ["**/*_test.go", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/test/**", "**/tests/**", "**/*-test.sh"] }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{ "class": "tests", "tier": "R0", "why": "test-only changes cannot break production behaviour",
"paths": ["**/*_test.go", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/test/**", "**/tests/**", "*-test.sh"] }
{ "class": "tests", "tier": "R0", "why": "test-only changes cannot break production behaviour",
"paths": ["**/*_test.go", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/test/**", "**/tests/**", "**/*-test.sh"] }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-risk/risk-map.v0.json` around lines 53 - 54, Update the "tests"
class path patterns in the risk-map configuration so the shell-test glob matches
files at any directory depth, not only root-level files. Change the existing
"*-test.sh" pattern to its recursive equivalent while preserving the other test
path patterns and classification.

Comment on lines +31 to +33
"permitted_paths": ["go.mod", "go.sum", "**/go.mod", "**/go.sum", "**/package.json", "**/package-lock.json",
"**/pnpm-lock.yaml", "**/yarn.lock", "**/requirements*.txt", "**/pyproject.toml",
"**/uv.lock", "**/Cargo.toml", "**/Cargo.lock", ".github/workflows/**"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align permitted_paths with the deps class in the risk map.

The deps rule in scripts/pr-risk/risk-map.v0.json (lines 39-43) lists Gemfile, Gemfile.lock, composer.json, composer.lock, *.gemspec, poetry.lock, Pipfile, Pipfile.lock, npm-shrinkwrap.json, setup.py, and go.work*. This list omits all of them. A Ruby or PHP dependabot PR therefore fails paths_ok, falls back to human, and records a shape_failure that says dependabot failed to look like dependabot. The overall tier does not move, because the path floor pins those manifests at R3 anyway — but the noise trains readers to skip the one field designed to catch real impersonation.

Two manifest lists that must agree are one list too many. Consider deriving the permitted set from the deps rule, or at minimum extend it here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/pr-risk/runbook-registry.v0.json` around lines 31 - 33, Update the
permitted_paths configuration associated with the deps rule to include every
dependency-manifest pattern listed by deps in risk-map.v0.json, including
Gemfile, Gemfile.lock, composer.json, composer.lock, *.gemspec, poetry.lock,
Pipfile, Pipfile.lock, npm-shrinkwrap.json, setup.py, and go.work*. Prefer
deriving this set from the deps rule if the registry supports it; otherwise
extend the existing list while preserving all current entries.

@mattmillerai mattmillerai added the cursor-review Multi-model cursor review label Aug 3, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Cursor Review — Consolidated panel

Triggered by @mattmillerai.

Found 10 finding(s).

Severity Count
🟠 High 5
🟡 Medium 5

Panel: 7/8 reviewers contributed findings.

Reviewers that did not contribute: claude-opus-5-thinking-max:edge-case (empty)

checks_pending_excl_self:$checks.pending,
outcome:(if .mergedAt != null then "merged" elif .state == "CLOSED" then "closed_unmerged" else "open" end),
changed_paths:(if (.files.pageInfo.hasNextPage // false) then null
else [.files.nodes[] | {path:.path, additions:.additions, deletions:.deletions, change_type:.changeType}] end),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High — Changed-file records keep only path and change_type, so a RENAMED file is recorded solely under its destination path and the previous path is discarded. Renaming .github/workflows/deploy.yml, an auth/ file, or a migration to an innocuous location therefore escapes both the sensitive path floor and the DELETED-under-sensitive-class rule. Add previousFilePath to the GraphQL files selection and feed it into the path-floor and deletion checks.
Raised by 2 of 8 reviewers (gpt-5.6-sol-max adversarial, gpt-5.6-sol-max edge-case).

then "fork / author-association were not collected (\($pvst)) — the `external` provenance class is un-decidable, and defaulting it to 'not a fork' would silently retire the external => R3 rule"
else "PR author did not resolve to a GitHub account — provenance is unattributable" end),
provenance:null}
else {tier: (($M.provenance_tiers // {})[$prov] // "R1"), status:"ok",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highread_map only checks that the tier VALUES in provenance_tiers are known tiers; it never requires the external key. A repo map that omits it passes validation and then ($M.provenance_tiers[$prov] // "R1") grades fork and first-time-contributor PRs R1, silently retiring the "external is R3, no exceptions" invariant the map comment and README both promise. Require external (and ideally all four classes) in the structural validation, or fall back to R3 rather than R1 for a missing key.
Raised by 3 of 8 reviewers (gpt-5.6-sol-max adversarial, gpt-5.6-sol-max edge-case, kimi-k2.7-code adversarial).

then {state: null, pending: false, status: "unknown"}
else
([$ro.contexts.nodes[]
| select(((.__typename == "CheckRun") and ((.checkSuite.workflowRun.workflow.name // "") == $self)) | not)]) as $ctx

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High — Self-exclusion drops every CheckRun whose checkSuite.workflowRun.workflow.name equals $self, not just this job's own check run. A consumer that puts the pr-risk job in an existing CI workflow loses all its sibling jobs from the rollup, so a FAILED test job is excluded and the remaining green contexts aggregate to SUCCESS — reversibility then grades R0/R1 on a red PR. Match on the run/job ID (e.g. GITHUB_RUN_ID via checkSuite.workflowRun.databaseId) instead of the display name.
Raised by 3 of 8 reviewers (claude-opus-5-thinking-max adversarial, gpt-5.6-sol-max adversarial, gpt-5.6-sol-max edge-case).

-H "Accept: application/vnd.github.raw" > repo-risk-map.json 2>/dev/null; then
echo "map=repo-risk-map.json" >> "$GITHUB_OUTPUT"
echo "using ${MAP_PATH} from ${BASE_REF}"
else

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High — The override fetch treats ANY non-zero gh api exit as "file absent" (stderr is discarded by 2>/dev/null), so a 403 rate-limit, 5xx, or network blip silently grades the PR against the generic default map instead of the repo's sharpened one — a lower tier computed from an input nobody read, which is what the unknown contract forbids everywhere else. Distinguish a genuine 404 (e.g. capture the status via gh api --include/-i or check the error text) and fail loudly on anything else.
Raised by 4 of 8 reviewers (claude-opus-5-thinking-max adversarial, gpt-5.6-sol-max adversarial, gpt-5.6-sol-max edge-case, kimi-k2.7-code adversarial).

else
([$A1.classes[]? | select(. as $c | ($RV.irreversible_classes // []) | index($c))]) as $irrev
| ([$paths[] | select(.change_type == "DELETED") | .path]) as $deleted
| (($A1.classes // []) | any(. as $c | ($RV.delete_sensitive_classes // []) | index($c))) as $del_sensitive

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highdel_sensitive is computed from $A1.classes — the classes matched by ANY changed file — and then combined with "the PR deletes at least one file". A PR that modifies an auth file while deleting an unrelated README therefore reports "deletes N file(s) under a sensitive class" and pins reversibility at R3. Compute the sensitive-class match over the DELETED paths only.
Raised by 3 of 8 reviewers (gemini-3.1-pro adversarial, gemini-3.1-pro edge-case, gpt-5.6-sol-max edge-case).

elif any($ctx[]; (.__typename == "CheckRun" and ((.status != "COMPLETED") or ((.conclusion // "") == "STALE")))
or (.__typename == "StatusContext" and (.state | IN("PENDING","EXPECTED"))))
then "PENDING"
elif ($ctx | length) > 0 then "SUCCESS"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — Any nonempty set of completed, non-failing contexts aggregates to SUCCESS, including a rollup where every CheckRun concluded SKIPPED, NEUTRAL, or null. Those establish nothing about whether tests ran, yet they let reversibility drop to R0/R1 — the axis's own stated question is "did tests covering these lines actually run". Require at least one SUCCESS conclusion before aggregating to SUCCESS.
Raised by 1 of 8 reviewers (gpt-5.6-sol-max edge-case).

*) echo "grader failed (rc=$rc)"; exit 1 ;; # setup/usage bug: fail loud
esac
pending="$(jq -r '.checks_pending_excl_self // false' record.json)"
if [ "$pending" = "true" ] && [ "$(date +%s)" -lt "$deadline" ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — The poll exits as soon as no CURRENTLY VISIBLE context is pending, so a rollup that is empty or transiently all-green (checks that have not registered yet) ends the wait and applies a low-risk label that a later failure never revises. Consider requiring the rollup to be non-empty and stable across two consecutive polls, or documenting that late-registering checks are not reflected.
Raised by 2 of 8 reviewers (gpt-5.6-sol-max adversarial, gpt-5.6-sol-max edge-case).

for l in "${OWNED[@]}"; do
[ "$l" = "$TARGET" ] && continue
if has "$l"; then
gh api -X DELETE "repos/$REPO/issues/$PR_NUMBER/labels/$l" >/dev/null 2>&1 \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — Label names are interpolated raw into REST paths (.../labels/$l here and the GET at line 105). A remapped label containing a space, /, #, ?, or % — all legal in GitHub labels — produces a malformed or misrouted request; the DELETE failure is then treated as a write failure and hard-fails the run with exit 4. Percent-encode the segment (jq -Rr @​uri) before building the path.
Raised by 5 of 8 reviewers (claude-opus-5-thinking-max adversarial, gemini-3.1-pro adversarial, gemini-3.1-pro edge-case, gpt-5.6-sol-max adversarial+edge-case, kimi-k2.7-code adversarial+edge-case).

fi

# Current labels on the PR (a PR is an issue to the labels API).
current="$(gh api "repos/$REPO/issues/$PR_NUMBER/labels" --jq '[.[].name]' 2>/dev/null)" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — The current-label read has no --paginate, so only the first 30 labels are seen. On a PR carrying more than 30 labels a stale grader-owned label falls off the page, is never removed, and the PR displays two contradictory risk tiers — breaking the "exactly one grader label" ownership contract this script is built around.
Raised by 5 of 8 reviewers (claude-opus-5-thinking-max adversarial, gemini-3.1-pro adversarial, gemini-3.1-pro edge-case, gpt-5.6-sol-max adversarial+edge-case).

number title state isDraft createdAt updatedAt closedAt mergedAt
author{ login } authorAssociation baseRefName headRefName isCrossRepository
additions deletions changedFiles
labels(first:20){ nodes{ name } }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumlabels(first:20) has no pageInfo { hasNextPage } guard, unlike files(first:100) and contexts(first:100) which both degrade to unknown on truncation. A PR with more than 20 labels yields an incomplete labels array and a wrong agent_coded flag, presenting a truncated read as a confident provenance grade.
Raised by 5 of 8 reviewers (claude-opus-5-thinking-max adversarial, gemini-3.1-pro adversarial+edge-case, gpt-5.6-sol-max adversarial+edge-case, kimi-k2.7-code adversarial+edge-case).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cursor-review Multi-model cursor review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants