pi-pr-review 1.17.7 → 1.17.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +2 -2
- package/extensions/review-table.ts +9 -4
- package/lib/pr-review-artifacts.ts +9 -0
- package/lib/pr-review-markdown.ts +62 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.17.8](https://github.com/10ego/pi-pr-review/compare/v1.17.7...v1.17.8) (2026-09-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **review:** publish retained incomplete findings ([#138](https://github.com/10ego/pi-pr-review/issues/138)) ([586205b](https://github.com/10ego/pi-pr-review/commit/586205bbe46b50e04f00d53d43b3c353c3bea95e))
|
|
9
|
+
|
|
3
10
|
## [1.17.7](https://github.com/10ego/pi-pr-review/compare/v1.17.6...v1.17.7) (2026-09-01)
|
|
4
11
|
|
|
5
12
|
|
package/README.md
CHANGED
|
@@ -195,7 +195,7 @@ Every invocation has a host-owned monotonic 15-minute hard cap, including the tw
|
|
|
195
195
|
|
|
196
196
|
A timed-out or retryable quota/rate-limit/capacity lane may start at most one configured fallback attempt. It starts only when at least `minimumFallbackMs` plus cleanup reserve remains; the host never changes the configured model, thinking level, or tool policy to save time. If a tier is unset, its existing nearest-configured-tier/Pi-default behavior is unchanged.
|
|
197
197
|
|
|
198
|
-
On an attempt deadline the host records timeout separately from user cancellation, sends TERM to the original child, waits only `terminationGraceMs`, then sends KILL if no exit was observed and stops draining after the cleanup reserve. Partial assistant text and telemetry survive this lifecycle. The synthesis cap arms only once review work goes quiet: any turn that starts or any review tool that runs again while the cap is armed defers it, and it re-arms from the next turn end, so early review-tool turns (for example verification discovery) cannot starve later heavy lanes. Batch/total expiry stops queued work and waiting lanes; completed and partial artifacts proceed to Markdown synthesis or deterministic lane assembly, identify every incomplete reviewer, and remain eligible for concise `COMMENT` publication. Complete contract-valid candidate blocks retained before a timeout are deterministically recovered as canonical findings, including when terminal synthesis is
|
|
198
|
+
On an attempt deadline the host records timeout separately from user cancellation, sends TERM to the original child, waits only `terminationGraceMs`, then sends KILL if no exit was observed and stops draining after the cleanup reserve. Partial assistant text and telemetry survive this lifecycle. The synthesis cap arms only once review work goes quiet: any turn that starts or any review tool that runs again while the cap is armed defers it, and it re-arms from the next turn end, so early review-tool turns (for example verification discovery) cannot starve later heavy lanes. Batch/total expiry stops queued work and waiting lanes; completed and partial artifacts proceed to Markdown synthesis or deterministic lane assembly, identify every incomplete reviewer, and remain eligible for concise `COMMENT` publication. Complete contract-valid candidate blocks retained before a timeout are deterministically recovered as canonical findings from the terminal lane output and retained primary/fallback attempt history, including when a later attempt or terminal synthesis is empty or malformed. Host-recorded attempt ordinals—not array position—define chronology; duplicate ordinals disable history recovery, a later exact clean contract supersedes older provisional findings, and truncated blocks or arbitrary lane prose remain private. A timed-out lane is never reported as `NO FINDINGS` or full coverage, and a lane ended by the host total or synthesis deadline is disclosed with that kind (`deadline_expired`) instead of being mistaken for its own attempt deadline expiring. Host lane artifacts are authoritative for completeness in both directions: a false assistant completion claim cannot upgrade incomplete lanes, and a paraphrased or omitted `Lane completeness` line cannot downgrade a host-complete batch away from the concise renderer.
|
|
199
199
|
|
|
200
200
|
Initial operating targets are ordinary-review p50 ≤ 6 minutes and p95 ≤ 12 minutes, and large-review p50 ≤ 10 minutes and p95 ≤ 14 minutes, with the 15-minute hard cap authoritative. Invocation telemetry starts before GitHub preflight and records configured deadline source/caps, termination grace, cleanup reserve, and active wall time; batch details record invocation time consumed before each attempt, batch/total time remaining at dispatch, first event/output timing, lifecycle counts, configured and effective batch-truncated attempt deadlines, fallback starts/budget rejections, external total/synthesis deadline expiries per lane, and termination grace/escalation data. These are initial production targets, not a promise that every provider completes before its host deadline.
|
|
201
201
|
|
|
@@ -231,7 +231,7 @@ Review semantics are Markdown-first. A deterministic tolerant parser normalizes
|
|
|
231
231
|
|
|
232
232
|
You can publish the cache later with `/pr-review-publish 123`, or directly ask the agent to “post the inline review,” “manually post the review comments,” “post it as an inline review,” or “publish the review for PR #123.” The extension handles that request directly before an agent turn. `/pr-review-publish` and a matching direct request publish only the cache; they never start or rerun review agents. Unnumbered direct requests select the latest cached review for the current repository. Only fresh interactive/RPC input can use the direct path.
|
|
233
233
|
|
|
234
|
-
Every authorized publish path builds one GitHub review payload and sends at most one review `POST`; it never submits `REQUEST_CHANGES` or retries a rejected write with a fallback POST. Fully parsed Markdown with one complete retained artifact for every host-registered dispatch may emit a gated `APPROVE` through the same host binding, priority, lifecycle, self-author, and stale checks as strict JSON. Host lane evidence is authoritative for completeness: a paraphrased or omitted `All requested lanes completed.` disclosure cannot downgrade a host-complete batch, and a false disclosure cannot upgrade incomplete or missing lane coverage; the assistant's canonical line is consulted only when no batch evidence exists. Verdict fields outside the document preamble, hidden in CommonMark code/HTML blocks, or inside lazy container continuations, severity-tagged headings outside `Findings`, incomplete or missing host lane coverage, malformed or unsafe output, and lane-fallback artifacts remain `COMMENT` reviews; their safely parsed findings keep inline placement, while unparsable output publishes body-only. Cache restore reclassifies persisted lane output and requires the exact host-recorded artifact key set, with every complete artifact matching the frozen invocation generation, tier, and minor-hygiene contract, and re-synthesizes the retained raw text under that frozen binding before retaining approval eligibility. For a current, open PR, the first 50 eligible P0–P3 findings with valid, unique diff anchors are inline. The concise top-level body starts with the verdict, points readers to inline findings, and places nits, off-diff findings, unavailable diff metadata, duplicate anchors, and overflow under `Other Notes`; overview, verification, strengths, completion warnings, and transport diagnostics stay out of the public summary. The complete original Markdown and retained lane evidence remain internal and are never substituted for a concise GitHub review, including degraded, incomplete, cached, direct, and stale `COMMENT` publication. If the concise body plus its canonical marker exceeds GitHub's limit, publication fails closed rather than dumping the full report. Stale or authorized non-open reviews are body-only. A stale approval additionally requires `allowStaleApprovals: true`.
|
|
234
|
+
Every authorized publish path builds one GitHub review payload and sends at most one review `POST`; it never submits `REQUEST_CHANGES` or retries a rejected write with a fallback POST. Fully parsed Markdown with one complete retained artifact for every host-registered dispatch may emit a gated `APPROVE` through the same host binding, priority, lifecycle, self-author, and stale checks as strict JSON. Host lane evidence is authoritative for completeness: a paraphrased or omitted `All requested lanes completed.` disclosure cannot downgrade a host-complete batch, and a false disclosure cannot upgrade incomplete or missing lane coverage; the assistant's canonical line is consulted only when no batch evidence exists. Verdict fields outside the document preamble, hidden in CommonMark code/HTML blocks, or inside lazy container continuations, severity-tagged headings outside `Findings`, incomplete or missing host lane coverage, malformed or unsafe output, and lane-fallback artifacts remain `COMMENT` reviews; their safely parsed findings keep inline placement, while unparsable output publishes body-only. Cache restore reclassifies persisted lane output and requires the exact host-recorded artifact key set, with every complete artifact matching the frozen invocation generation, tier, and minor-hygiene contract, and re-synthesizes the retained raw text under that frozen binding before retaining approval eligibility. For a current, open PR, the first 50 eligible P0–P3 findings with valid, unique diff anchors are inline. The concise top-level body starts with the verdict, points readers to inline findings, and places nits, off-diff findings, unavailable diff metadata, duplicate anchors, and overflow under `Other Notes`; overview, verification, strengths, completion warnings, and transport diagnostics stay out of the public summary. Incomplete/degraded reviews publish whenever synthesis, retained attempts, or validated extraction produced at least one canonical finding. If none did, the completed record remains available privately and GitHub publication is skipped rather than creating a verdict-only review. The complete original Markdown and retained lane evidence remain internal and are never substituted for a concise GitHub review, including degraded, incomplete, cached, direct, and stale `COMMENT` publication. If the concise body plus its canonical marker exceeds GitHub's limit, publication fails closed rather than dumping the full report. Stale or authorized non-open reviews are body-only. A stale approval additionally requires `allowStaleApprovals: true`.
|
|
235
235
|
|
|
236
236
|
All publication paths apply host-enforced safety gates: captured posting authority, repository and requested-PR binding, reviewed/current-head and stale policy, bounded bodies and payloads, draft and lifecycle checks, non-open authorization, authenticated-identity same-head duplicate detection, and a final head check. Fully or partially parsed review paths additionally enforce safe inline locations, including degraded incomplete-lane reviews whose findings still parse. Raw and lane-assembled fallbacks without parsed findings are body-only; every degraded path is `COMMENT`-only, and assistant text cannot select event, commit, repository, hostname, API path, or inline anchors. Unknown or invalid host states fail closed before a write.
|
|
237
237
|
|
|
@@ -461,6 +461,14 @@ async function publishCompletedReview(
|
|
|
461
461
|
}
|
|
462
462
|
const degradedPublication = record.completeness === "incomplete" ||
|
|
463
463
|
(record.synthesisQuality !== undefined && record.synthesisQuality !== "fully_parsed");
|
|
464
|
+
const findings = Array.isArray(record.review.findings) ? record.review.findings : [];
|
|
465
|
+
if (degradedPublication && findings.length === 0) {
|
|
466
|
+
// A result-less degraded record remains cached for private diagnostics and a
|
|
467
|
+
// later rerun, but must not create a verdict-only GitHub review. Incomplete
|
|
468
|
+
// reviews with any validated synthesis/lane/extraction finding still publish.
|
|
469
|
+
ctx.ui.notify("PR review was not posted: the incomplete review retained no validated findings", "warning");
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
464
472
|
const result = await publishPullReview({
|
|
465
473
|
cwd: ctx.cwd,
|
|
466
474
|
prNumber: record.invocation.prNumber,
|
|
@@ -475,9 +483,6 @@ async function publishCompletedReview(
|
|
|
475
483
|
// inline findings. Retained raw synthesis and lane evidence remain private
|
|
476
484
|
// completion diagnostics, including for degraded/incomplete COMMENT runs.
|
|
477
485
|
// If the concise payload cannot fit, publication fails closed.
|
|
478
|
-
forceBodyOnly: record.synthesisQuality !== undefined &&
|
|
479
|
-
record.synthesisQuality !== "fully_parsed" &&
|
|
480
|
-
!(Array.isArray(record.review.findings) && record.review.findings.length > 0),
|
|
481
486
|
// A publication body identifies Markdown-derived or degraded synthesis.
|
|
482
487
|
// Enforce COMMENT again at the final publication boundary so restored
|
|
483
488
|
// canonical artifacts created by an older parser cannot inherit APPROVE.
|
|
@@ -1076,7 +1081,7 @@ export default function registerReviewTable(
|
|
|
1076
1081
|
}
|
|
1077
1082
|
}
|
|
1078
1083
|
const publishResult = await publishCompletedReview(pending.record, { kind: "frozen-invocation" }, ctx);
|
|
1079
|
-
if (pending.extraction) {
|
|
1084
|
+
if (pending.extraction && publishResult) {
|
|
1080
1085
|
try {
|
|
1081
1086
|
// `published` decorates exactly the attempt whose merged event it
|
|
1082
1087
|
// follows: the spread carries that attempt's identity, so tooling can
|
|
@@ -480,6 +480,15 @@ function parseIntegratedCompletion(text: string): boolean {
|
|
|
480
480
|
* output. A timed-out final block may be truncated; earlier complete blocks
|
|
481
481
|
* remain usable, while arbitrary prose and unsafe containers recover nothing.
|
|
482
482
|
*/
|
|
483
|
+
export function isValidatedReviewLaneNoFindings(
|
|
484
|
+
rawText: string,
|
|
485
|
+
expectedOutput: "review_lane" | "nonempty" = "review_lane",
|
|
486
|
+
): boolean {
|
|
487
|
+
const text = normalizeReviewText(rawText);
|
|
488
|
+
if (expectedOutput === "review_lane") return /^NO FINDINGS\.?$/.test(text.trim());
|
|
489
|
+
return parseIntegratedCompletion(text) && text.split("\n").some((line) => line === NO_FINDINGS_SENTINEL);
|
|
490
|
+
}
|
|
491
|
+
|
|
483
492
|
export function extractValidatedReviewLaneCandidates(
|
|
484
493
|
rawText: string,
|
|
485
494
|
expectedOutput: "review_lane" | "nonempty" = "review_lane",
|
|
@@ -2,6 +2,7 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
extractValidatedReviewLaneCandidates,
|
|
5
|
+
isValidatedReviewLaneNoFindings,
|
|
5
6
|
type ExpectedReviewLane,
|
|
6
7
|
type ReviewLaneArtifact,
|
|
7
8
|
} from "./pr-review-artifacts.ts";
|
|
@@ -492,6 +493,39 @@ function retainedLaneText(lane: ReviewLaneArtifact): string {
|
|
|
492
493
|
return "";
|
|
493
494
|
}
|
|
494
495
|
|
|
496
|
+
function retainedLaneCandidateTexts(
|
|
497
|
+
lane: ReviewLaneArtifact,
|
|
498
|
+
contract: "review_lane" | "nonempty",
|
|
499
|
+
): string[] {
|
|
500
|
+
const texts: string[] = [];
|
|
501
|
+
const seen = new Set<string>();
|
|
502
|
+
const retain = (value: string): "continue" | "stop" => {
|
|
503
|
+
const text = value.trim();
|
|
504
|
+
if (!text || seen.has(text)) return "continue";
|
|
505
|
+
seen.add(text);
|
|
506
|
+
// A later exact clean contract supersedes provisional findings from older
|
|
507
|
+
// attempts. Malformed or empty later output does not.
|
|
508
|
+
if (isValidatedReviewLaneNoFindings(text, contract)) return "stop";
|
|
509
|
+
texts.push(text);
|
|
510
|
+
return "continue";
|
|
511
|
+
};
|
|
512
|
+
// The lane-level text is the latest authoritative output. Earlier attempts
|
|
513
|
+
// remain eligible only for independently contract-valid findings; malformed
|
|
514
|
+
// prose can neither become a finding nor poison a valid earlier attempt.
|
|
515
|
+
if (retain(lane.rawText) === "stop") return texts;
|
|
516
|
+
const ordinals = new Set<number>();
|
|
517
|
+
for (const attempt of lane.attempts) {
|
|
518
|
+
if (ordinals.has(attempt.ordinal)) return texts;
|
|
519
|
+
ordinals.add(attempt.ordinal);
|
|
520
|
+
}
|
|
521
|
+
const newestFirst = [...lane.attempts].sort((left, right) =>
|
|
522
|
+
left.ordinal === right.ordinal ? 0 : left.ordinal > right.ordinal ? -1 : 1);
|
|
523
|
+
for (const attempt of newestFirst) {
|
|
524
|
+
if (retain(attempt.rawText) === "stop") break;
|
|
525
|
+
}
|
|
526
|
+
return texts;
|
|
527
|
+
}
|
|
528
|
+
|
|
495
529
|
function retainedLaneFindings(
|
|
496
530
|
lanes: readonly ReviewLaneArtifact[],
|
|
497
531
|
expected: readonly ExpectedReviewLane[],
|
|
@@ -500,32 +534,34 @@ function retainedLaneFindings(
|
|
|
500
534
|
const seen = new Set<string>();
|
|
501
535
|
for (const lane of lanes) {
|
|
502
536
|
const contract = expected.find((descriptor) => descriptor.key === lane.key)?.expectedOutput ?? "review_lane";
|
|
503
|
-
for (const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
537
|
+
for (const text of retainedLaneCandidateTexts(lane, contract)) {
|
|
538
|
+
for (const candidate of extractValidatedReviewLaneCandidates(text, contract)) {
|
|
539
|
+
if (!candidate.prRelated) continue;
|
|
540
|
+
const parsedLocation = candidate.location === "repo-wide"
|
|
541
|
+
? { status: "absent" as const, location: null }
|
|
542
|
+
: parseLocation(`${candidate.location} ${candidate.side}`);
|
|
543
|
+
if (parsedLocation.status === "unsafe") continue;
|
|
544
|
+
const codeLocation = parsedLocation.location
|
|
545
|
+
? { ...parsedLocation.location, commentable: candidate.inDiff }
|
|
546
|
+
: null;
|
|
547
|
+
const key = JSON.stringify([
|
|
548
|
+
candidate.severity,
|
|
549
|
+
candidate.title,
|
|
550
|
+
candidate.why,
|
|
551
|
+
candidate.location,
|
|
552
|
+
candidate.side,
|
|
553
|
+
]);
|
|
554
|
+
if (seen.has(key)) continue;
|
|
555
|
+
seen.add(key);
|
|
556
|
+
findings.push({
|
|
557
|
+
title: candidate.title,
|
|
558
|
+
severity: candidate.severity,
|
|
559
|
+
blocking: candidate.severity === "P0" || candidate.severity === "P1",
|
|
560
|
+
body: candidate.why,
|
|
561
|
+
confidence_score: candidate.confidence,
|
|
562
|
+
code_location: codeLocation,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
529
565
|
}
|
|
530
566
|
}
|
|
531
567
|
return findings;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pr-review",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.8",
|
|
4
4
|
"description": "Parallel AI code review for GitHub pull requests in the Pi coding agent, with model-agnostic tiered subagents, structured findings, optional verification, and host-gated COMMENT or qualified APPROVE publishing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|