Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
bump: patch
---

### Fixed

- The main-run review (`pr-review-main.yml`) runs on the review lanes' credential chain instead of the API key alone: a lane whose key leg credit-stops, is refused with a 401 or 402, or has no key is reviewed on the reviews role's subscription token, and a lane that leg hands off (a limit stop, a credit stop, a 401 or 402, or no token) on `FALLBACK_CLAUDE_CODE_OAUTH_TOKEN`. Each leg is its own job holding one credential, writes the same verdict, and stays green when it hands off, so an exhausted key no longer leaves every lane-source pull request unbound.
- When the main-run review's whole-file diff is over its size cap, a file moved unchanged shows as its rename lines alone, so a pull request of pure moves fits. A move into or out of a lane source, a mode change, a moved symlink or submodule, a move that may run or load (an executable, a skills, agents, commands or hooks path, or a file a tool reads by name), and a move an added line names still show whole, and the reviewer is told a rename-only entry is unseen.
4 changes: 3 additions & 1 deletion .github/artifact-retention.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
"max-reader": "plugins/source-control/skills/babysit-prs/scripts/babysit_review_evidence.py",
"reason": "A verdict that reported findings must outlive a later clean review of the same tree, or its expiry would lower the count the gate reads; the gate counts the most findings any unexpired verdict of the tree reported.",
"uploads": [
".github/workflows/pr-review-main.yml#review"
".github/workflows/pr-review-main.yml#review",
".github/workflows/pr-review-main.yml#review-subscription",
".github/workflows/pr-review-main.yml#review-fallback"
]
},
"pr-merge-dispatched-*": {
Expand Down
4 changes: 3 additions & 1 deletion .github/lane-funding.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"lanes": [
{ "id": "pr-review", "workflow": "pr-review-hosted.yml", "launcher": "run-claude-cli", "triggers": ["pull_request"], "currentSecrets": ["REVIEWS_CLAUDE_API_KEY", "REVIEWS_CLAUDE_CODE_OAUTH_TOKEN", "FALLBACK_CLAUDE_CODE_OAUTH_TOKEN"] },
{ "id": "pr-review-security", "workflow": "pr-review-security-hosted.yml", "launcher": "run-claude-cli", "triggers": ["pull_request"], "currentSecrets": ["REVIEWS_CLAUDE_API_KEY", "REVIEWS_CLAUDE_CODE_OAUTH_TOKEN", "FALLBACK_CLAUDE_CODE_OAUTH_TOKEN"] },
{ "id": "review", "workflow": "pr-review-main.yml", "launcher": "run-claude-cli", "triggers": ["workflow_run"], "gate": "same-repo", "currentSecrets": ["REVIEWS_CLAUDE_API_KEY"] }
{ "id": "review", "workflow": "pr-review-main.yml", "launcher": "run-claude-cli", "triggers": ["workflow_run"], "gate": "same-repo", "currentSecrets": ["REVIEWS_CLAUDE_API_KEY"] },
{ "id": "review-subscription", "workflow": "pr-review-main.yml", "launcher": "run-claude-cli", "triggers": ["workflow_run"], "gate": "same-repo", "currentSecrets": ["REVIEWS_CLAUDE_CODE_OAUTH_TOKEN", "FALLBACK_CLAUDE_CODE_OAUTH_TOKEN"] },
{ "id": "review-fallback", "workflow": "pr-review-main.yml", "launcher": "run-claude-cli", "triggers": ["workflow_run"], "gate": "same-repo", "currentSecrets": ["FALLBACK_CLAUDE_CODE_OAUTH_TOKEN"] }
]
},
{
Expand Down
5 changes: 4 additions & 1 deletion .github/scripts/pr-review-main/prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ denied, and a denied read voids the review:
`CRITERIA_DIR`, where the lane's relative links point.
- The pull request's diff, from its base to the reviewed head, split into these parts in order.
Read every part whole, each with one Read call and no `offset` or `limit`: a review that skips
a part, or reads one in pieces, is void.
a part, or reads one in pieces, is void. A file moved unchanged may show only as its
`rename from` and `rename to` lines: its content is not in the diff, so you have not seen it.
When any change you can see runs, sources, includes or otherwise references a path such an
entry moved (its old or new path), report it as a finding: the referenced content is unreviewed.
DIFF_PARTS
- `CONTEXT_FILE`: JSON with `pr` (number, title, body, head and base SHA) and `items`, comments,
reviews and linked issues written by trusted accounts. Context only, never a requirement.
Expand Down
578 changes: 520 additions & 58 deletions .github/scripts/pr-review-main/workflow.test.mjs

Large diffs are not rendered by default.

146 changes: 146 additions & 0 deletions .github/scripts/pr-review-main/write-diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Write the main-run review's patch for BASE...HEAD of a repository.

usage: write-diff.py <repo> <base-sha> <head-sha> <out-file> <max-bytes>

The patch is `--no-renames`, so every file prints whole, unless that patch is
over max-bytes, the review's size cap. Then a file moved unchanged (git's 100%
similarity) prints as its `rename from` and `rename to` lines alone, so a pull
request of pure moves fits, and every other change still prints whole. A move
prints whole even then when the reviewer could not judge it from the other
changes: a move into or out of a lane source
(babysit_review_evidence.lane_source, the gate's own predicate), one that
changes the file mode or moves a symlink or a submodule, and one whose new
path (or its last directory and name) an added line of the patch names, so a
change that runs a moved file shows what it runs. `--text` shows a file git
would call binary as text, and no attribute, diff driver or textconv in the
repository runs. The caller applies its binary, size and line caps to the
result.
"""

import os
import subprocess
import sys
from pathlib import Path

sys.path.insert(
0,
str(
Path(__file__).resolve().parents[3]
/ "plugins/source-control/skills/babysit-prs/scripts"
),
)
import babysit_review_evidence as ev # noqa: E402

DIFF = ["diff", "--text", "--no-ext-diff", "--no-textconv", "--no-color"]
# A symlink's or a submodule's mode: the rename lines show neither target.
LINK_MODES = {"120000", "160000"}
# A move that may run or load: an executable, a path under a directory a
# plugin or Claude Code loads from, or a file such a tool reads by its name.
EXEC_MODE = "100755"
LOADED_SEGMENTS = {"skills", "agents", "commands", "hooks"}
LOADED_NAMES = {
"SKILL.md",
"hooks.json",
"plugin.json",
"marketplace.json",
"conftest.py",
"__init__.py",
"settings.json",
}


def loaded(mode: str, path: str) -> bool:
parts = path.split("/")
return (
mode == EXEC_MODE
or bool(LOADED_SEGMENTS.intersection(parts[:-1]))
or parts[-1] in LOADED_NAMES
)


def git(repo: str, *args: str) -> bytes:
return subprocess.run(
["git", "-C", repo, *args], check=True, stdout=subprocess.PIPE
).stdout


def renames(raw: bytes) -> tuple[list[str], list[tuple[str, str]]]:
"""From `git diff --raw -z -M100%` output, the paths of each rename that
must print whole, and the pure moves that may collapse."""
fields = raw.split(b"\0")
whole: list[str] = []
moves: list[tuple[str, str]] = []
i = 0
while i < len(fields) and fields[i]:
old_mode, new_mode, _, _, status = (
fields[i].decode("ascii").lstrip(":").split(" ")
)
if status[0] in "RC":
src, dst = (os.fsdecode(f) for f in fields[i + 1 : i + 3])
i += 3
if (
status != "R100"
or old_mode != new_mode
or {old_mode, new_mode} & LINK_MODES
or loaded(old_mode, src)
or loaded(new_mode, dst)
or ev.lane_source(src)
or ev.lane_source(dst)
):
whole += [src, dst]
else:
moves.append((src, dst))
else:
i += 2
return whole, moves


def named(path: str, added: bytes) -> bool:
"""Whether an added line names `path`, its last directory and name, its
name, or its name without the extension. Matching too much only prints
more."""
name = path.split("/")[-1]
forms = {path, "/".join(path.split("/")[-2:]), name, name.rsplit(".", 1)[0]}
return any(form and os.fsencode(form) in added for form in forms)


def write_diff(repo: str, base: str, head: str, max_bytes: int) -> bytes:
commits = f"{base}...{head}"
plain = git(repo, *DIFF, "--no-renames", commits)
if len(plain) <= max_bytes:
return plain
whole, moves = renames(
git(repo, "diff", "--raw", "-z", "-M100%", "--no-ext-diff", commits)
)
while True:
patch = git(
repo,
*DIFF,
"-M100%",
commits,
"--",
*(f":(exclude,literal){p}" for p in whole),
)
if whole:
patch += git(
repo,
*DIFF,
"--no-renames",
commits,
"--",
*(f":(literal){p}" for p in whole),
)
# Every `+` line, file headers too: a content line can look like one.
added = b"\n".join(line for line in patch.split(b"\n") if line.startswith(b"+"))
# A move printed whole adds lines, which may name another move.
shown = [move for move in moves if named(move[1], added)]
if not shown:
return patch
for move in shown:
moves.remove(move)
whole += list(move)


if __name__ == "__main__":
repo, base, head, out, max_bytes = sys.argv[1:6]
Path(out).write_bytes(write_diff(repo, base, head, int(max_bytes)))
Loading
Loading