Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
d46480a
feat: per-severity warning and outofscope caps in the quality gate
sblattj Sep 24, 2026
cb32673
feat(quality): title similarity scorer for reworded duplicates (#10 T1)
sblattj Sep 24, 2026
793432d
feat(rules): match_globs, the applies_to path matcher (#12 T1)
sblattj Sep 24, 2026
0e51db2
refactor(forges): share the diff renderer and generalize GitHub paging
sblattj Sep 24, 2026
791f638
feat: prompt-template override loader, validation and record (#11 T1)
sblattj Sep 24, 2026
c3e18b7
merge: seat/Q10-A into release/0.15.0 (0.15.0 wave 1, seat Q10-A)
sblattj Sep 24, 2026
8d36b2e
merge: seat/Q13-C into release/0.15.0 (0.15.0 wave 1, seat Q13-C)
sblattj Sep 24, 2026
0bf23de
feat(config): land the seven 0.15.0 configuration keys
sblattj Sep 24, 2026
3adc5c1
feat(eval): judge prompt, response parser and per-ref cap (#14 T5)
sblattj Sep 24, 2026
47774b9
merge: seat/F15-A into release/0.15.0 (0.15.0 wave 1, seat F15-A)
sblattj Sep 24, 2026
022c91b
merge: seat/P11-A into release/0.15.0 (0.15.0 wave 1, seat P11-A)
sblattj Sep 24, 2026
0e0616b
merge: seat/P12-A into release/0.15.0 (0.15.0 wave 1, seat P12-A)
sblattj Sep 24, 2026
823cbbd
feat: eval case loader and validation (#14 T1)
sblattj Sep 24, 2026
4a172f4
merge: seat/FND into release/0.15.0 (0.15.0 wave 1, seat FND)
sblattj Sep 24, 2026
ded1364
feat: PRHistory and pure description pinning for replays (#16 T1)
sblattj Sep 24, 2026
c4166b3
merge: seat/E14-E into release/0.15.0 (0.15.0 wave 1, seat E14-E)
sblattj Sep 24, 2026
ce7d7fd
feat: parse and validate applies_to in rules front matter (#12 T2)
sblattj Sep 24, 2026
f7fc6fa
feat(eval): deterministic must_match matcher and score metrics (#14 T4)
sblattj Sep 24, 2026
188a21b
merge: seat/E14-A into release/0.15.0 (0.15.0 wave 1, seat E14-A)
sblattj Sep 24, 2026
aa6403b
merge: seat/R16-A into release/0.15.0 (0.15.0 wave 1, seat R16-A)
sblattj Sep 24, 2026
0a9c28b
feat(quality): reworded-duplicate tier in apply_sweep_dedup (#10 T2)
sblattj Sep 24, 2026
25ce3fa
merge: seat/P12-B into release/0.15.0 (0.15.0 wave 1, seat P12-B)
sblattj Sep 24, 2026
8ac7cd8
feat(cli): prxref prompts export DIR [--force] (#11 T5)
sblattj Sep 24, 2026
18e3659
feat: render worker/systemic prompt overrides from PromptContext (#11…
sblattj Sep 24, 2026
b402f65
merge: seat/E14-D into release/0.15.0 (0.15.0 wave 1, seat E14-D)
sblattj Sep 24, 2026
8548225
feat(github): rebuild a PR diff from the /pulls/{n}/files listing
sblattj Sep 24, 2026
6c97945
merge: seat/Q10-B into release/0.15.0 (0.15.0 wave 2, seat Q10-B)
sblattj Sep 24, 2026
e092cef
merge: seat/P11-E into release/0.15.0 (0.15.0 wave 2, seat P11-E)
sblattj Sep 24, 2026
e12b081
merge: seat/F15-B into release/0.15.0 (0.15.0 wave 2, seat F15-B)
sblattj Sep 24, 2026
fc457be
fix: GitHub diff past 20,000 lines falls back to the files listing (#…
sblattj Sep 24, 2026
60b6635
merge: seat/P11-B into release/0.15.0 (0.15.0 wave 2, seat P11-B)
sblattj Sep 24, 2026
07903cc
merge: seat/F15-C into release/0.15.0 (0.15.0 wave 2, seat F15-C)
sblattj Sep 24, 2026
6ae4b61
feat(cli): --as-of, --description-file and --no-description replay flags
sblattj Sep 24, 2026
e733301
feat(eval): judge client, per-case judge call, grade cache, cost and …
sblattj Sep 24, 2026
71b4bac
feat: add the prxref eval run|score|compare subparser and dispatch (#…
sblattj Sep 24, 2026
f8dc24e
merge: seat/R16-C into release/0.15.0 (0.15.0 wave 2, seat R16-C)
sblattj Sep 24, 2026
07892c1
merge: seat/E14-F into release/0.15.0 (0.15.0 wave 2, seat E14-F)
sblattj Sep 24, 2026
0926d71
feat: optional per-finding rule and its parser gate (#13 T1)
sblattj Sep 24, 2026
3cf9dcb
feat(github): read PR description and title history over GraphQL (#16…
sblattj Sep 24, 2026
f1f5b80
feat: load path-scoped review rules (issue #12 T3)
sblattj Sep 24, 2026
7ee797c
merge: seat/E14-B into release/0.15.0 (0.15.0 wave 2, seat E14-B)
sblattj Sep 24, 2026
16cfb96
feat: wire PRXREF_DEDUP_SIMILARITY into the sweep dedup (#10 T4, T5)
sblattj Sep 24, 2026
804b57a
feat(bitbucket): read PR description history from /activity (#16 T7)
sblattj Sep 24, 2026
52df282
merge: seat/R16-B into release/0.15.0 (0.15.0 wave 2, seat R16-B)
sblattj Sep 24, 2026
0f63e54
merge: seat/Q13-A into release/0.15.0 (0.15.0 wave 2, seat Q13-A)
sblattj Sep 24, 2026
5788ff3
merge: seat/Q10-C into release/0.15.0 (0.15.0 wave 2, seat Q10-C)
sblattj Sep 24, 2026
31a9dae
test: end-to-end coverage of GitHub's 406 too_large -> files fallback…
sblattj Sep 24, 2026
5c5316d
merge: seat/P12-C into release/0.15.0 (0.15.0 wave 2, seat P12-C)
sblattj Sep 24, 2026
328d1c9
merge: seat/F15-D into release/0.15.0 (0.15.0 wave 3, seat F15-D)
sblattj Sep 24, 2026
66deeb7
fix(bitbucket): never let a current-state snapshot veto recorded history
sblattj Sep 24, 2026
5dd93ab
feat: wire prompt-template overrides through orchestrate_review (#11 T3)
sblattj Sep 24, 2026
ecd0216
merge: seat/R16-G into release/0.15.0 (0.15.0 wave 2, seat R16-G)
sblattj Sep 24, 2026
fedd543
merge: seat/P11-C into release/0.15.0 (0.15.0 wave 3, seat P11-C)
sblattj Sep 24, 2026
e6e34b4
feat: pin a --pr-url replay's title and description to a cutoff (#16 T4)
sblattj Sep 24, 2026
75fef9a
merge: seat/R16-D into release/0.15.0 (0.15.0 wave 3, seat R16-D)
sblattj Sep 24, 2026
8434824
feat: prxref eval run replays every case into a labelled run director…
sblattj Sep 24, 2026
9e39729
test: keep the replay seam tests off the network under default pinning
sblattj Sep 24, 2026
840c62a
feat: select scoped review rules per unit and build each unit's block…
sblattj Sep 24, 2026
81a7cfb
feat: add the rule-grouping quality pass (#13 T3)
sblattj Sep 24, 2026
9c36b0c
merge: seat/E14-C into release/0.15.0 (0.15.0 wave 3, seat E14-C)
sblattj Sep 24, 2026
e1c064d
feat: ask for a per-finding rule when grouping is on (#13 T2)
sblattj Sep 24, 2026
df75f96
merge: seat/P12-D into release/0.15.0 (0.15.0 wave 4, seat P12-D)
sblattj Sep 24, 2026
8af1c4a
test: pin rule_request by its slot, not as the last PromptContext field
sblattj Sep 24, 2026
7776190
merge: seat/Q13-D into release/0.15.0 (0.15.0 wave 4, seat Q13-D)
sblattj Sep 24, 2026
e3d5574
test: compare the unset-prompts trace as a multiset, not an order
sblattj Sep 24, 2026
cdb3e9e
merge: seat/Q13-B into release/0.15.0 (0.15.0 wave 4, seat Q13-B)
sblattj Sep 24, 2026
2b8c8da
feat: stamp which PR description a replay saw, and document pinning (…
sblattj Sep 24, 2026
4d6fad7
feat: --prompts-dir on review, and the prompt_templates stamp in JSON…
sblattj Sep 24, 2026
e5b949b
merge: seat/P11-D into release/0.15.0 (0.15.0 wave 4, seat P11-D)
sblattj Sep 24, 2026
0fcd890
feat: prxref eval score writes score.json and score.md (#14 T7)
sblattj Sep 24, 2026
8fc1a70
merge: seat/R16-E into release/0.15.0 (0.15.0 wave 4, seat R16-E)
sblattj Sep 24, 2026
fc56850
merge: seat/E14-G into release/0.15.0 (0.15.0 wave 4, seat E14-G)
sblattj Sep 24, 2026
221e371
feat: wire path-scoped review rules through the orchestrator (#12 T5)
sblattj Sep 24, 2026
2c3d932
feat: wire finding grouping and the per-severity caps through the review
sblattj Sep 24, 2026
caf0dc9
merge: seat/P12-E into release/0.15.0 (0.15.0 wave 5, seat P12-E)
sblattj Sep 24, 2026
b90738b
feat: judge tier credits a grouped finding per location (#14)
sblattj Sep 24, 2026
86eef67
merge: seat/Q13-E into release/0.15.0 (0.15.0 wave 5, seat Q13-E)
sblattj Sep 24, 2026
45473c4
merge: seat/E14-K into release/0.15.0 (0.15.0 wave 5, seat E14-K)
sblattj Sep 24, 2026
214eae6
feat: prxref eval compare prints two scored runs side by side (#14 T8)
sblattj Sep 24, 2026
d688965
merge: seat/E14-H into release/0.15.0 (0.15.0 wave 5, seat E14-H)
sblattj Sep 24, 2026
79e54db
feat: emit each finding's rule and grouped locations in JSON and text…
sblattj Sep 24, 2026
e58d2bd
feat: --scoped-rules flag, run-record surfaces and docs for path-scop…
sblattj Sep 24, 2026
593633b
merge: seat/Q13-F into release/0.15.0 (0.15.0 wave 6, seat Q13-F)
sblattj Sep 24, 2026
0d0f961
merge: seat/P12-F into release/0.15.0 (0.15.0 wave 6, seat P12-F)
sblattj Sep 24, 2026
b4e9e9d
docs: the prxref eval reference, pinned to the code it documents
sblattj Sep 24, 2026
7d49e0d
test: eval run, score and compare end to end through cli.main (#14 T9)
sblattj Sep 24, 2026
fe3cab2
feat: eval run --scoped-rules and --prompts-dir, and run.json records…
sblattj Sep 24, 2026
f837e06
merge: seat/E14-I into release/0.15.0 (0.15.0 wave 6, seat E14-I)
sblattj Sep 24, 2026
44b9bcd
merge: seat/E14-J into release/0.15.0 (0.15.0 wave 6, seat E14-J)
sblattj Sep 24, 2026
bf4fd31
merge: release/0.15.0 (44b9bcd) into seat/E14-L
sblattj Sep 24, 2026
962731f
merge: seat/E14-L into release/0.15.0 (0.15.0 wave 7, seat E14-L)
sblattj Sep 24, 2026
3898f17
fix(github): read a diff past 20,000 lines from compare, fail closed …
sblattj Sep 24, 2026
76ce0ba
fix(github): warn on a withheld patch, and name the 300-file 406
sblattj Sep 24, 2026
1ae91c9
merge: seat/F15-E into release/0.15.0 (0.15.0 wave 8, seat F15-E)
sblattj Sep 24, 2026
9cc6df6
fix: drop findings that echo the prompt's example, and a false size c…
sblattj Sep 24, 2026
1421933
merge: seat/Q-ECHO into release/0.15.0 (0.15.0 wave 8, seat Q-ECHO)
sblattj Sep 24, 2026
436c570
fix: read GitHub's raw+json file media type as the file
sblattj Sep 24, 2026
e638ced
docs: 0.15.0 release docs sweep (REL-A)
sblattj Sep 24, 2026
a13bf0a
merge: seat/X-FC into release/0.15.0 (0.15.0 wave 9, seat X-FC)
sblattj Sep 24, 2026
9e36eb5
merge: seat/REL-A into release/0.15.0 (0.15.0 wave 9, seat REL-A)
sblattj Sep 24, 2026
75e8f75
release: 0.15.0 version bump and CHANGELOG
sblattj Sep 24, 2026
2dbe25a
docs: say prompt_templates stamps every template file the directory h…
sblattj Sep 24, 2026
f7484ff
docs: rewrite HANDOFF.md for v0.15.0
sblattj Sep 24, 2026
ba21d1f
merge: seat/REL-B into release/0.15.0 (0.15.0 wave 9, seat REL-B)
sblattj Sep 24, 2026
05dd29c
docs: match the file-content fix's own scope in HANDOFF.md
sblattj Sep 24, 2026
d33d80a
merge: seat/REL-C into release/0.15.0 (0.15.0 wave 9, seat REL-C)
sblattj Sep 24, 2026
ae73a31
feat(quality): per-rule cap pass apply_rule_cap and rule_cap_counts (…
sblattj Sep 24, 2026
76b298d
feat: PRXREF_MAX_FINDINGS_PER_RULE, the per-rule cap's config key (#18)
sblattj Sep 24, 2026
60b5a23
merge: seat/R18-A into release/0.15.0 (0.15.0 wave 10, seat R18-A)
sblattj Sep 24, 2026
a81fb53
merge: seat/R18-B into release/0.15.0 (0.15.0 wave 10, seat R18-B)
sblattj Sep 24, 2026
b5d331d
docs: 0.15.0 file-content fix, live checks and final release edits
sblattj Sep 24, 2026
bad7c9a
merge: seat/REL-D into release/0.15.0 (0.15.0 wave 10, seat REL-D)
sblattj Sep 24, 2026
3af7ed7
feat: wire the per-rule cap into the review, run record and JSON (#18)
sblattj Sep 24, 2026
0972406
merge: seat/R18-C into release/0.15.0 (0.15.0 wave 11, seat R18-C)
sblattj Sep 24, 2026
6a43eb5
docs: release notes, handoff and rule_counts docs for the per-rule ca…
sblattj Sep 24, 2026
9711717
fix: keep the chunk/sweep boundary exact across the quality gate
sblattj Sep 24, 2026
000cfee
merge: seat/REL-E into release/0.15.0 (0.15.0 wave 12, seat REL-E)
sblattj Sep 24, 2026
f4ceaf8
merge: seat/R18-D into release/0.15.0 (0.15.0 wave 12, seat R18-D)
sblattj Sep 24, 2026
7d8969e
docs: release notes for the sweep-boundary fix, final test count
sblattj Sep 24, 2026
508ab38
docs: HANDOFF records the review of the release PR itself
sblattj Sep 24, 2026
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
64 changes: 64 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,45 @@ PRXREF_CONFIDENCE_FLOOR=0.6
# (Legacy alias: PRXREF_MAX_ERRORS.)
PRXREF_MAX_ERROR_FINDINGS=10

# Per-severity caps (0.15.0): max warning-severity findings reported per
# review, the excess dropped lowest-confidence-first. Must be >= 0; 0 caps
# every warning. Empty (the default) = unlimited.
# PRXREF_MAX_WARNING_FINDINGS=

# Per-severity caps (0.15.0): the same cap for the minor tier, findings of
# severity "outofscope". That is a severity, NOT ticket scope "out" from
# PRXREF_TICKET_CONTEXT_FILE. "spec" findings are never capped. Must be >= 0;
# empty (the default) = unlimited.
# PRXREF_MAX_OUTOFSCOPE_FINDINGS=

# Per-rule cap (0.15.0): the most findings one team rule may produce in a
# review, across files. Applies only when a review rules file
# (PRXREF_REVIEW_RULES or PRXREF_SCOPED_RULES) is loaded, and then the model
# is asked to name the rule it applied. The excess fold into the best one
# (highest severity, then confidence), which lists them as "Also at: ...";
# findings with no rule fold on their normalized title. Whole-PR sweep
# findings are never capped. Must be >= 0; 0 turns it off. Default 2.
# PRXREF_MAX_FINDINGS_PER_RULE=2

# Finding grouping (0.15.0): set to exactly 1 to fold findings that break the
# same rule in the same file into one comment listing the other locations
# ("Also at: ..."); without a rule, the normalized title groups them. Runs
# before the caps, so PRXREF_MAX_ERROR_FINDINGS counts groups. The rule label
# is one line (whitespace runs collapse to a space) of at most 120 characters;
# a longer one, or one holding a control, surrogate, private-use or bidi
# control character, is dropped and the finding groups by title. Every
# --format json finding row carries "rule" and "locations" (null when unset).
# Default off: prompts and output are unchanged, apart from those two JSON
# keys being null.
# PRXREF_GROUP_FINDINGS=0

# Reworded-duplicate dedup (0.15.0): two findings on the same file and line
# whose titles score at or above this Jaccard similarity (and share at least
# 3 title words) are one finding; a chunk copy beats a sweep copy of equal or
# lower severity. Greater than 0 and at most 1.0. Empty (the default) = off:
# only the exact-title dedup runs. Not yet tuned on real review data.
# PRXREF_DEDUP_SIMILARITY=

# Max diff chunks reviewed per PR (default 8). Must be > 0.
PRXREF_MAX_CHUNKS=8

Expand Down Expand Up @@ -204,6 +243,31 @@ PRXREF_MAX_CHUNKS=8
# a longer body is truncated with a warning. Must be > 0.
# PRXREF_REVIEW_RULES_MAX_CHARS=12000

# Path-scoped review rules (0.15.0): rules files and directories (*.md, one
# level deep) whose `applies_to:` front-matter globs pick the chunks each file
# reaches; the whole-PR sweep gets the union. Added to PRXREF_REVIEW_RULES,
# never replacing it. A file without applies_to reaches every unit; a URL,
# `applies_to: []` or a malformed file is a configuration error (exit 2).
# Comma- or whitespace-separated; repeatable `--scoped-rules PATH` flags
# replace this list. Empty (the default) = off. Read from a trusted checkout.
# PRXREF_SCOPED_RULES=

# Path-scoped review rules (0.15.0): characters of scoped-rules text one
# review unit (a chunk or the sweep) receives. Whole files go in, in load
# order, while they fit; the file that overflows is truncated to the room
# left (or left out when no room is left), every later file is left out, and
# one warning per run names this variable. Must be > 0.
# PRXREF_SCOPED_RULES_MAX_CHARS=24000

# Prompt template overrides (0.15.0): directory of replacement worker.md,
# systemic.md and summary.md; an absent file keeps the packaged template. Each
# one is validated before any network call, and a bad one is a configuration
# error (exit 2). `--prompts-dir DIR` wins for one run; `prxref prompts export
# DIR` writes the packaged templates to start from. Read it from a trusted
# checkout: a PR that commits a template rewrites its own review. Empty (the
# default) = the packaged templates.
# PRXREF_PROMPTS_DIR=

# Plain-text or Markdown file holding the ticket this PR implements. When set,
# every finding is marked in, out of, or of unknown ticket scope. An empty
# file means "this PR has no ticket". A missing or non-UTF-8 file is a
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ __pycache__/
dist/
*.egg-info/
.env
prxref-eval/
.superpowers/
.claude/
docs/superpowers/
Expand Down
393 changes: 393 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

706 changes: 475 additions & 231 deletions HANDOFF.md

Large diffs are not rendered by default.

104 changes: 91 additions & 13 deletions README.md

Large diffs are not rendered by default.

26 changes: 18 additions & 8 deletions docs/env-vars.md

Large diffs are not rendered by default.

838 changes: 838 additions & 0 deletions docs/evals.md

Large diffs are not rendered by default.

16 changes: 13 additions & 3 deletions docs/forges.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Every host is covered, but not by the same means. GitHub and GitLab are host-agn
- **Payload:** PR URL read from `pullrequest.links.html.href`.
- **Signature Header:** `X-Hub-Signature` (HMAC-SHA256) validated against `PRXREF_BITBUCKET_WEBHOOK_SECRET`.
- **Pinned Commit Range (Replay):** `GET /2.0/repositories/{owner}/{repo}/diff/{head_sha}..{base_sha}?topic=true` with `Accept: text/plain`, returning the changes on the head side of the merge-base. Bitbucket spells a range SOURCE..DEST, the reverse of git, so the head SHA comes first; the other order names the reverse range and returns a different diff that still parses. `topic=true` is the merge-base ("three-dot") form and is sent explicitly rather than left to the default, because `topic=false` diffs the two commits directly and so also shows whatever landed on the base after the fork. The text is returned unmodified, an empty range returns empty text, and an HTTP or transport error raises.
- **Description History (Replay):** `GET /2.0/repositories/{owner}/{repo}/pullrequests/{number}` (the creation date, the author, and the current title and description), then the pull request's `GET …/pullrequests/{number}/activity` feed (`pagelen=50`, following `next`, at most 50 pages), then `GET /2.0/repositories/{owner}/{repo}/commit/{sha}` for the head commit's `date` (`--head-sha` when given, else the pull request's source commit). Each `update` entry's `changes.description` carries the old and the new text with the update's `date`, and `changes.title` a rename. The first human review is the earliest approval, change request or comment by a user account (not an app) other than the pull request's author, skipping deleted comments and prxref's own posts. A history that cannot be trusted whole pins nothing: a feed longer than the page budget, an unreadable change entry, or edits that do not chain to the current text leave the current title and description in place, with a warning. The requests carry the same credentials as everything above, and a public repository needs none. On 2026-09-24 the reader was run live, read-only and with no token, against a public pull request whose description had been edited: it read the whole history, took the approval as the first review (an app's comment was skipped), and pinned the text in force then. A later live check confirmed the rename shape: on three renamed public pull requests `changes.title` carried the old and the new title as `{old, new}` strings, and the reader pinned the right title at every cutoff tried across one pull request's two renames. The `changes_requested` entry a change request writes has not been seen live yet (none in the four activity feeds read), so the reader assumes it has an `approval`'s shape, a `user` and a `date`. Anonymous reads are limited to 60 an hour (`x-ratelimit-limit: 60;w=3600`), and one history read costs at least three of them (the pull request, each activity page, and the head commit), on top of the review's own reads, so replays without a token can exhaust the limit. A history read that fails, a `429` that outlasts the adapter's retries included, never fails the review: the replay keeps the current title and description and logs a WARNING (`replay shows the PR's CURRENT title and description: reading its description history failed …`).

---

Expand All @@ -53,16 +54,17 @@ Every host is covered, but not by the same means. GitHub and GitLab are host-agn
- **API Endpoints & Behavior:**
- **Base URL:** `https://api.github.com` for `github.com`, or `https://{host}/api/v3` for GHES.
- **Metadata:** `GET /repos/{owner}/{repo}/pulls/{number}`
- **Diffs:** `GET /repos/{owner}/{repo}/pulls/{number}` with `Accept: application/vnd.github.v3.diff, application/vnd.diff`. GitHub refuses this diff for a pull request whose diff runs past 20,000 lines (HTTP `406`, error code `too_large`), so such a PR ends as an `Error` run and gets the error notice. There is no fallback to the paged `/pulls/{number}/files` listing yet.
- **Diffs:** `GET /repos/{owner}/{repo}/pulls/{number}` with `Accept: application/vnd.github.v3.diff, application/vnd.diff`. GitHub refuses this diff for a pull request whose diff runs past 20,000 lines or 300 files, answering HTTP `406` with error code `too_large` and a message naming the limit hit. On exactly that answer the pull request is read once as JSON (**Metadata** above) for its `base.sha`, `head.sha`, `changed_files`, `additions` and `deletions`, and the diff is read from `GET /repos/{owner}/{repo}/compare/{base.sha}...{head.sha}` with `Accept: application/vnd.github.diff`, the endpoint of the pinned-range replay below, which applies neither limit. That diff is used only when its file count and its added and removed line counts equal the pull request's own; the switch is then logged at debug only, with GitHub's message, and adds nothing to the summary. When the compare read fails (an HTTP error or a timeout, say) or its counts differ, one warning names the reason and the diff is rebuilt from the paged `GET /repos/{owner}/{repo}/pulls/{number}/files` listing (`per_page=100`, read to the last page), with the same header reconstruction as the GitLab adapter's **Diffs**. The listing is checked, not trusted: it must hold the pull request's `changed_files` entries (GitHub stops it at 3,000 files), and its entries' `additions` and `deletions` must sum to the pull request's. The sums matter because GitHub has been seen to drop the `patch` of ordinary text files from a large listing page and list those files with zero changed lines. Any miss, or a listing page that cannot be read, fails the review with verdict `Error` rather than reviewing part of it. A file listed without a `patch` is reviewed as a header-only file. With no changed lines (an empty file, a pure rename, a binary, a mode change) that is logged at debug only. With changed lines, GitHub withheld the patch but kept the file's true counts, as it has been seen to do for whole-file lockfile adds and removes, and one warning names the file and those counts. A `406` without `too_large` (a wrong media type, say) still fails the review, and a pull request under both limits makes the same single request as before.
- **Summary Comments:** Managed on the issue comments endpoint (`/repos/{owner}/{repo}/issues/{number}/comments`). Summary deduplication is handled via the embedded hidden HTML marker `<!-- prxref-summary -->`. If an existing review comment contains this marker, it is updated via `PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}` instead of creating a duplicate comment.
- **Inline Comments:** `POST /repos/{owner}/{repo}/pulls/{number}/comments` with `body`, `path`, `line`, and `side` (`RIGHT`). HTTP 422 errors (e.g. comment line not part of diff hunk) are gracefully skipped.
- **Thread List:** `GET /repos/{owner}/{repo}/pulls/{number}/comments`.
- **File Content:** `GET /repos/{owner}/{repo}/contents/{path}?ref={sha}` with `Accept: application/vnd.github.raw+json`, best-effort, read with the same token as everything else above (no extra scope). A 403/404, a JSON body (directory or a file over the 1 MB raw ceiling), oversize, or binary body returns `None` and is logged at debug, never a hard error.
- **File Content:** `GET /repos/{owner}/{repo}/contents/{path}?ref={sha}` with `Accept: application/vnd.github.raw+json`, best-effort, read with the same token as everything else above (no extra scope). The response is judged by its media type (the `Content-Type` before any `;`, case-insensitive), never by a substring: github.com labels a regular file `application/vnd.github.raw+json`, and that raw media type (or GitHub's older `application/vnd.github.raw` / `application/vnd.github.v3.raw`) or any `text/*` type is read as the file, while a JSON envelope (`application/json`, which is what a directory returns, or any other `+json` type) returns `None`. A 403/404, a JSON envelope, oversize, or binary body returns `None` and is logged at debug, never a hard error.
- **Webhook Integration:**
- **Event Header:** `X-GitHub-Event` (must equal `pull_request`)
- **Accepted Actions:** `opened`, `synchronize`
- **Signature Header:** `X-Hub-Signature-256` (HMAC-SHA256) validated against `PRXREF_GITHUB_WEBHOOK_SECRET`.
- **Pinned Commit Range (Replay):** `GET /repos/{owner}/{repo}/compare/{base_sha}...{head_sha}` on the same base URL (GHES included), with `Accept: application/vnd.github.diff`. The three dots are the merge-base form and are required, because the two-dot spelling returns 404. Without the diff media type the endpoint returns its JSON comparison object rather than a diff. The text is returned unmodified, an empty range (a head already merged into the base) returns empty text, and an HTTP or transport error raises.
- **Pinned Commit Range (Replay):** `GET /repos/{owner}/{repo}/compare/{base_sha}...{head_sha}` on the same base URL (GHES included), with `Accept: application/vnd.github.diff`. The three dots are the merge-base form and are required, because the two-dot spelling returns 404. Without the diff media type the endpoint returns its JSON comparison object rather than a diff. The text is returned unmodified, an empty range (a head already merged into the base) returns empty text, and an HTTP or transport error raises. The compare endpoint applies neither of the pull-request diff's limits, 20,000 lines or 300 files: two read-only probes of public tag ranges on 2026-09-24, one of about 1.0 million diff lines across 3,316 files and one of about 1.05 million lines across 10,373 files, each returned HTTP `200` with the whole diff. So a `--base-sha`/`--head-sha` replay is unaffected by either limit and has no files-listing fallback.
- **Description History (Replay):** one GraphQL query, `POST https://api.github.com/graphql` for `github.com` or `POST https://{host}/api/graphql` on GHES, reads the pull request's description versions (`userContentEdits`, one full text per edit, ordered by `editedAt`), every title rename (the `RENAMED_TITLE_EVENT` timeline), its reviews and conversation comments, and the head commit's `committedDate` (of `--head-sha` when given, else of the pull request's last commit). A connection longer than one page (100 nodes) is read by a further POST, up to 50 in all. The first human review is the earliest submitted review or comment by a `User` (not a bot) other than the pull request's author, skipping prxref's own posts. GitHub's GraphQL API refuses anonymous reads, so this needs `PRXREF_GITHUB_TOKEN` (or `PRXREF_GITHUB_ENTERPRISE_TOKEN` on GHES); without one, and on any failed read, the replay keeps the current title and description and logs a warning. The fields the query reads were checked live against public `github.com` pull requests while the reader was designed, and its tests use recorded response shapes. The GHES endpoint has **not been probed**.

---

Expand All @@ -87,6 +89,7 @@ Every host is covered, but not by the same means. GitHub and GitLab are host-agn
- **Accepted Actions:** `open`, `update`
- **Signature Header:** `X-Gitlab-Token` (plain secret token) validated against `PRXREF_GITLAB_WEBHOOK_SECRET`.
- **Pinned Commit Range (Replay):** `GET /repository/compare?from={base_sha}&to={head_sha}&straight=false`. `straight=false` is the merge-base form; `straight=true` would diff the two commits directly. `unidiff` is deliberately not requested, so each entry's `diff` holds only its hunks, and the entries are rendered by the same header reconstruction as **Diffs** above. A response with `compare_timeout: true` raises rather than reviewing an incomplete file list. An entry flagged `too_large` or `collapsed` carries no inline diff: it is logged as a warning and reviewed as a header-only file. An empty range returns empty text, and an HTTP or transport error raises.
- **Description History (Replay):** none. The adapter cannot read an MR's description history, so a `--pr-url` replay here shows the MR's current title and description and logs a warning, and `--as-of` exits `2`. GitLab's system notes answered anonymous reads with HTTP `401`, and whether they carry the old description text is unverified.

---

Expand Down Expand Up @@ -178,6 +181,10 @@ paging rather than `page`/`pagelen`. It therefore gets its own adapter.
have **not been probed against a live Data Center**. No minimum version is claimed, but one
Atlassian knowledge-base article reports that the path-less `/diff` returns 400 on some older
versions.
- **Description History (Replay):** none. The adapter cannot read a pull request's
description history (whether the `/activities` feed carries the old text is unverified),
so a `--pr-url` replay here shows the current title and description and logs a warning,
and `--as-of` exits `2`.

---

Expand Down Expand Up @@ -341,3 +348,6 @@ Azure DevOps Services and Azure DevOps Server (on-prem): both speak REST
files that it left out. A range whose two ends are the same commit returned empty
text. Beyond that run, its tests use recorded response shapes. Azure DevOps Server is
untested, as above.
- **Description History (Replay):** none. The adapter cannot read a pull request's
description history, so a `--pr-url` replay here shows the current title and
description and logs a warning, and `--as-of` exits `2`.
Loading
Loading