pi-pr-review 1.14.0 → 1.14.2
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 +14 -0
- package/extensions/pr-review-subagent.ts +4 -3
- package/extensions/review-table.ts +14 -3
- package/lib/pr-review-extract.ts +32 -6
- package/lib/pr-review-loop.ts +15 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.14.2](https://github.com/10ego/pi-pr-review/compare/v1.14.1...v1.14.2) (2026-08-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **extract:** scan lane evidence past the synthesis findings framing ([#77](https://github.com/10ego/pi-pr-review/issues/77)) ([dfebc36](https://github.com/10ego/pi-pr-review/commit/dfebc3632910fdde46581d4a48d236d72c7046df))
|
|
9
|
+
|
|
10
|
+
## [1.14.1](https://github.com/10ego/pi-pr-review/compare/v1.14.0...v1.14.1) (2026-08-22)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **extract:** harden provenance and retained-binding handling from sample review ([#75](https://github.com/10ego/pi-pr-review/issues/75)) ([ff5206a](https://github.com/10ego/pi-pr-review/commit/ff5206ad94ec0a901c1a8398149d31445d49874b))
|
|
16
|
+
|
|
3
17
|
## [1.14.0](https://github.com/10ego/pi-pr-review/compare/v1.13.0...v1.14.0) (2026-08-22)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
} from "../lib/pr-review-artifacts.ts";
|
|
57
57
|
import { runWithConcurrency } from "../lib/pr-review-concurrency.ts";
|
|
58
58
|
import { attemptDeadline, fallbackBudget, type ReviewBudget } from "../lib/pr-review-deadlines.ts";
|
|
59
|
-
import { buildExtractionSystemPrompt, MAX_EXTRACTION_OUTPUT_BYTES } from "../lib/pr-review-extract.ts";
|
|
59
|
+
import { buildExtractionSystemPrompt, buildExtractionTask, MAX_EXTRACTION_OUTPUT_BYTES } from "../lib/pr-review-extract.ts";
|
|
60
60
|
import { loadReviewContext, shardUnifiedDiff } from "../lib/pr-review-context.ts";
|
|
61
61
|
import {
|
|
62
62
|
combineAbortSignals,
|
|
@@ -2727,7 +2727,7 @@ export async function runFindingExtraction(
|
|
|
2727
2727
|
invocation.command,
|
|
2728
2728
|
invocation.args,
|
|
2729
2729
|
ctx.cwd,
|
|
2730
|
-
input,
|
|
2730
|
+
buildExtractionTask(input),
|
|
2731
2731
|
lease.signal,
|
|
2732
2732
|
undefined,
|
|
2733
2733
|
undefined,
|
|
@@ -2738,7 +2738,8 @@ export async function runFindingExtraction(
|
|
|
2738
2738
|
},
|
|
2739
2739
|
);
|
|
2740
2740
|
// Output blast-radius bound: reject oversized documents before parsing.
|
|
2741
|
-
|
|
2741
|
+
// Measured in UTF-8 bytes to match the parser's own budget.
|
|
2742
|
+
if (Buffer.byteLength(result.text, "utf8") > MAX_EXTRACTION_OUTPUT_BYTES) {
|
|
2742
2743
|
return { text: "", exitCode: result.exitCode || 1, errorMessage: "extraction output exceeded the size cap" };
|
|
2743
2744
|
}
|
|
2744
2745
|
return {
|
|
@@ -552,10 +552,14 @@ export default function registerReviewTable(
|
|
|
552
552
|
// Telemetry is best-effort and must never affect the review.
|
|
553
553
|
}
|
|
554
554
|
};
|
|
555
|
-
// A stale generation (replacement, cancellation, session switch) drops
|
|
556
|
-
// the merge entirely; the deterministic artifact was never completed.
|
|
557
555
|
if (!loopCoordinator.isLeaseActive(deferred.lease, ctx)) {
|
|
556
|
+
// Deadline expiry retains the same generation for deterministic
|
|
557
|
+
// degraded synthesis, so completion proceeds with the deterministic
|
|
558
|
+
// artifact. Only a genuine replacement (new generation) drops it.
|
|
558
559
|
recordOutcome("aborted");
|
|
560
|
+
if (loopCoordinator.retainedGenerationIs(deferred.lease.generation, ctx)) {
|
|
561
|
+
return { artifact: deferred.artifact };
|
|
562
|
+
}
|
|
559
563
|
return undefined;
|
|
560
564
|
}
|
|
561
565
|
let result: Awaited<PendingExtraction["promise"]>;
|
|
@@ -570,8 +574,12 @@ export default function registerReviewTable(
|
|
|
570
574
|
// Re-check ownership after the suspension: a replacement, cancellation,
|
|
571
575
|
// or session switch that landed while awaiting must not have this review
|
|
572
576
|
// consume the successor's invocation, publish, or revoke its binding.
|
|
577
|
+
// Same-generation deadline expiry still completes deterministically.
|
|
573
578
|
if (!loopCoordinator.isLeaseActive(deferred.lease, ctx)) {
|
|
574
579
|
recordOutcome("aborted");
|
|
580
|
+
if (loopCoordinator.retainedGenerationIs(deferred.lease.generation, ctx)) {
|
|
581
|
+
return { artifact: deferred.artifact };
|
|
582
|
+
}
|
|
575
583
|
return undefined;
|
|
576
584
|
}
|
|
577
585
|
if (result.timedOut) {
|
|
@@ -1091,7 +1099,10 @@ export default function registerReviewTable(
|
|
|
1091
1099
|
if (active && artifact && artifact.quality !== "fully_parsed") {
|
|
1092
1100
|
const setting = resolveExtractionSetting(getAgentDir());
|
|
1093
1101
|
if (setting.warning) ctx.ui.notify(setting.warning, "warning");
|
|
1094
|
-
if (setting.enabled && !pendingExtraction) {
|
|
1102
|
+
if (setting.enabled && !pendingExtraction && !loopCoordinator.deadlineExpired()) {
|
|
1103
|
+
// acquire() must never see a deadline-expired binding: it would
|
|
1104
|
+
// clear the retained invocation that degraded synthesis still
|
|
1105
|
+
// needs. An expired deadline leaves no window for extraction.
|
|
1095
1106
|
const lease = loopCoordinator.acquire(ctx);
|
|
1096
1107
|
if (lease) {
|
|
1097
1108
|
const input = buildExtractionInput(artifact.rawText, laneArtifacts);
|
package/lib/pr-review-extract.ts
CHANGED
|
@@ -89,6 +89,8 @@ export function buildExtractionSystemPrompt(): string {
|
|
|
89
89
|
"When the document names a file for the finding, also include: path (repo-relative file path exactly as written), start_line and end_line (integers from the document when stated), side (\"RIGHT\" for added lines, \"LEFT\" for removed lines), and location_quote (a verbatim substring of the document containing that path).",
|
|
90
90
|
"Optionally include source (the lane or section the finding came from).",
|
|
91
91
|
"Rules: report only findings actually stated in the document; never invent, merge, or upgrade severity; keep the reviewer's own wording in title and body; omit path fields when no location is stated; do not report strengths, summaries, or coverage notes as findings.",
|
|
92
|
+
"The synthesis's own Findings section is NOT authoritative about what the document states: a review summary saying \"No findings\" while a retained lane section states a concrete defect means that defect IS stated in the document and must be extracted.",
|
|
93
|
+
"Findings appear in any format: ### severity blocks, bulleted field lists (title/severity/why/location), or prose. Scan every section, including retained lane output, before deciding the document states no findings.",
|
|
92
94
|
"If the document states no findings, return {\"findings\":[]}.",
|
|
93
95
|
"Do not use tools, read files, or modify anything.",
|
|
94
96
|
].join("\n");
|
|
@@ -112,6 +114,23 @@ function truncateWithMarker(text: string, maxBytes: number, omittedBytes?: numbe
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
/** Assemble the bounded extraction payload: synthesis first, then lanes in order, each truncated with its own byte-count marker. */
|
|
117
|
+
/**
|
|
118
|
+
* Host-authored framing wrapped around the data document. Without it the
|
|
119
|
+
* child treats the synthesis's own "Findings: No findings." framing as
|
|
120
|
+
* authoritative and misses defects stated in retained lane output (observed
|
|
121
|
+
* in the first production extraction run).
|
|
122
|
+
*/
|
|
123
|
+
export function buildExtractionTask(input: string): string {
|
|
124
|
+
return [
|
|
125
|
+
"Objective: Extract every concrete defect finding stated anywhere in this review document, including inside the retained lane output sections.",
|
|
126
|
+
"A \"No findings\" line written by the summary does not mean the document states no findings: check the lane sections too.",
|
|
127
|
+
"",
|
|
128
|
+
"--- Review document ---",
|
|
129
|
+
input,
|
|
130
|
+
"--- End of review document ---",
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
115
134
|
export function buildExtractionInput(
|
|
116
135
|
rawText: string,
|
|
117
136
|
lanes: readonly ReviewLaneArtifact[],
|
|
@@ -189,7 +208,11 @@ function safeRelativePath(value: string): boolean {
|
|
|
189
208
|
/** Verify a verbatim quote against the normalized input. */
|
|
190
209
|
export function verifyQuote(quote: string, normalizedInput: string): boolean {
|
|
191
210
|
if (!quote || quote.length < MIN_QUOTE_CHARS || byteLength(quote) > MAX_QUOTE_BYTES) return false;
|
|
192
|
-
|
|
211
|
+
const normalized = normalizeForQuote(quote);
|
|
212
|
+
// Whitespace-only or control-heavy quotes normalize away; the normalized
|
|
213
|
+
// form must itself clear the minimum length before it can match.
|
|
214
|
+
if (normalized.length < MIN_QUOTE_CHARS) return false;
|
|
215
|
+
return normalizedInput.includes(normalized);
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
function deriveBlocking(severity: string): boolean {
|
|
@@ -197,8 +220,10 @@ function deriveBlocking(severity: string): boolean {
|
|
|
197
220
|
}
|
|
198
221
|
|
|
199
222
|
function normalizeFinding(wire: ExtractedFindingWire): ReviewFindingLike | undefined {
|
|
200
|
-
|
|
201
|
-
|
|
223
|
+
// Non-string fields are contract violations, not values to stringify.
|
|
224
|
+
if (typeof wire.title !== "string" || typeof wire.body !== "string") return undefined;
|
|
225
|
+
const title = wire.title.trim();
|
|
226
|
+
const body = wire.body.trim();
|
|
202
227
|
const severity = String(wire.severity ?? "");
|
|
203
228
|
if (!title || byteLength(title) > MAX_TITLE_BYTES) return undefined;
|
|
204
229
|
if (!body || byteLength(body) > MAX_BODY_BYTES) return undefined;
|
|
@@ -263,6 +288,10 @@ export function parseExtractionOutput(text: string, input: string): ExtractionPa
|
|
|
263
288
|
if (!Array.isArray(record.findings)) {
|
|
264
289
|
return { ok: false, rejection: { kind: "rejected", reason: "findings was not an array" } };
|
|
265
290
|
}
|
|
291
|
+
// Bound the scan before any per-record provenance work.
|
|
292
|
+
if (record.findings.length > MAX_EXTRACTED_FINDINGS) {
|
|
293
|
+
return { ok: false, rejection: { kind: "rejected", reason: `more than ${MAX_EXTRACTED_FINDINGS} findings` } };
|
|
294
|
+
}
|
|
266
295
|
const normalizedInput = normalizeForQuote(input);
|
|
267
296
|
const findings: ReviewFindingLike[] = [];
|
|
268
297
|
let rejectedProvenance = 0;
|
|
@@ -308,9 +337,6 @@ export function parseExtractionOutput(text: string, input: string): ExtractionPa
|
|
|
308
337
|
}
|
|
309
338
|
findings.push(finding);
|
|
310
339
|
}
|
|
311
|
-
if (findings.length > MAX_EXTRACTED_FINDINGS) {
|
|
312
|
-
return { ok: false, rejection: { kind: "rejected", reason: `more than ${MAX_EXTRACTED_FINDINGS} findings` } };
|
|
313
|
-
}
|
|
314
340
|
return {
|
|
315
341
|
ok: true,
|
|
316
342
|
value: {
|
package/lib/pr-review-loop.ts
CHANGED
|
@@ -413,6 +413,21 @@ export class ReviewLoopCoordinator {
|
|
|
413
413
|
return this.binding?.deadlineKind !== undefined;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Whether `generation` still names the deadline-expired binding that is
|
|
418
|
+
* deliberately retained for deterministic degraded synthesis. True only for
|
|
419
|
+
* the same session/cwd binding with an active deadline kind — never for a
|
|
420
|
+
* replacement generation, a cleared loop, or a live lease.
|
|
421
|
+
*/
|
|
422
|
+
retainedGenerationIs(
|
|
423
|
+
generation: number,
|
|
424
|
+
ctx: Pick<ExtensionContext, "cwd" | "sessionManager">,
|
|
425
|
+
): boolean {
|
|
426
|
+
const binding = this.binding;
|
|
427
|
+
return !!binding && binding.generation === generation &&
|
|
428
|
+
binding.deadlineKind !== undefined && sameBinding(binding, ctx);
|
|
429
|
+
}
|
|
430
|
+
|
|
416
431
|
totalDeadlineExpired(): boolean {
|
|
417
432
|
return this.binding?.deadlineKind === "total";
|
|
418
433
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pr-review",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.2",
|
|
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",
|