pi-pr-review 1.17.1 → 1.17.3

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.17.3](https://github.com/10ego/pi-pr-review/compare/v1.17.2...v1.17.3) (2026-08-31)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **review:** preserve validated confidence ratings ([#128](https://github.com/10ego/pi-pr-review/issues/128)) ([27843b7](https://github.com/10ego/pi-pr-review/commit/27843b7c6469827e65fc5d7c69511b20be3615ba))
9
+
10
+ ## [1.17.2](https://github.com/10ego/pi-pr-review/compare/v1.17.1...v1.17.2) (2026-08-31)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **review:** keep degraded comments concise ([#126](https://github.com/10ego/pi-pr-review/issues/126)) ([912e474](https://github.com/10ego/pi-pr-review/commit/912e474d04be9db504d2ea83bcd9a7c86db2ca5b))
16
+
3
17
  ## [1.17.1](https://github.com/10ego/pi-pr-review/compare/v1.17.0...v1.17.1) (2026-08-31)
4
18
 
5
19
 
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 the safe body-only `COMMENT` publication path. 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.
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 the concise body-only `COMMENT` publication path. 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
 
@@ -227,11 +227,11 @@ Publishing is off by default.
227
227
 
228
228
  The extension owns normal publishing. Before review execution it captures repository, hostname, PR number/title, reviewed head, lifecycle state, posting/stale authority, and invocation identity independently of assistant text. After synthesis, it caches one validated completed review (the host-owned canonical artifact) per repository and PR in the current Pi session. `autoPostReviews` and `--comment` publish that cached review after completion; `--no-comment` suppresses publication for the run.
229
229
 
230
- Review semantics are Markdown-first. A deterministic tolerant parser normalizes line endings, extracts complete findings when possible, and ignores heading-like text inside CommonMark fenced-code and HTML-block contexts. Every degraded synthesis — ambiguous, partially parsed, incomplete lane coverage, or absent terminal synthesis — publishes one deterministic host-rendered `COMMENT` body with generic code-review labels: a `Coverage` section with the exact lane lifecycle disclosure, a `Findings` section with host-formatted parsed findings (never a clean-review claim when nothing parsed), and the complete original synthesis plus every retained lane artifact preserved verbatim under `Retained synthesis` / `Retained lane output` with heading levels shifted so they nest. A contradictory completion claim inside retained text is reconciled to the host verdict. Reserved markers are sanitized, size limits are enforced, and the canonical marker is appended only by host code. Optional formatting repair is never required and can never suppress the degraded fallback.
230
+ Review semantics are Markdown-first. A deterministic tolerant parser normalizes line endings, extracts complete findings when possible, and ignores heading-like text inside CommonMark fenced-code and HTML-block contexts. Every degraded synthesis — ambiguous, partially parsed, incomplete lane coverage, or absent terminal synthesis — is retained internally as one deterministic host-rendered artifact with generic code-review labels: a `Coverage` section with the exact lane lifecycle disclosure, a `Findings` section with host-formatted parsed findings (never a clean-review claim when nothing parsed), and the complete original synthesis plus every retained lane artifact preserved under `Retained synthesis` / `Retained lane output`. GitHub publication never dumps that artifact: it emits a concise `COMMENT` with a short incomplete-coverage or unstructured-output warning, validated inline findings, and summary-only `Other Notes`. Reserved markers are sanitized, size limits are enforced, and the canonical marker is appended only by host code. Optional formatting repair is never required and can never suppress the degraded fallback.
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,” “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, and transport diagnostics stay out of the public summary. The complete original Markdown remains retained internally and is never substituted for a fully parsed concise review. 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, 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`.
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
 
@@ -293,7 +293,7 @@ Each finding includes:
293
293
 
294
294
  - severity: `P0`, `P1`, `P2`, `P3`, or `nit`;
295
295
  - whether it blocks the verdict;
296
- - an explanation and confidence score;
296
+ - an explanation and independently validated confidence score (the host preserves the supplied 0.0–1.0 value instead of manufacturing `1.00`; scoreless or malformed current Markdown degrades fail-closed, and pre-fix cached completion records are invalidated);
297
297
  - a diff-anchored file and line range when available.
298
298
 
299
299
  | Severity | Meaning |
@@ -432,6 +432,16 @@ type ReviewPublicationOrigin =
432
432
  | { readonly kind: "publish-command"; readonly stalePolicy: "frozen" | "allow-stale" }
433
433
  | { readonly kind: "direct-request" };
434
434
 
435
+ function conciseDegradedPublicationNotice(record: CompletedReviewRecord): string | undefined {
436
+ if (record.completeness === "incomplete") {
437
+ return "> [!WARNING]\n> Review coverage was incomplete. This COMMENT is not evidence of a clean review.";
438
+ }
439
+ if (record.synthesisQuality !== undefined && record.synthesisQuality !== "fully_parsed") {
440
+ return "> [!WARNING]\n> The review output was not fully structured. This COMMENT is not evidence of a clean review.";
441
+ }
442
+ return undefined;
443
+ }
444
+
435
445
  async function publishCompletedReview(
436
446
  record: CompletedReviewRecord,
437
447
  origin: ReviewPublicationOrigin,
@@ -459,8 +469,9 @@ async function publishCompletedReview(
459
469
  ctx.ui.notify("PR review was not posted: cached review artifact is missing pr.head_sha", "error");
460
470
  return undefined;
461
471
  }
462
- const useDegradedPublicationBody = typeof record.publicationBody === "string" &&
463
- (record.synthesisQuality !== "fully_parsed" || record.completeness === "incomplete");
472
+ const degradedPublication = record.completeness === "incomplete" ||
473
+ (record.synthesisQuality !== undefined && record.synthesisQuality !== "fully_parsed");
474
+ const publicationNotice = conciseDegradedPublicationNotice(record);
464
475
  const result = await publishPullReview({
465
476
  cwd: ctx.cwd,
466
477
  prNumber: record.invocation.prNumber,
@@ -471,21 +482,18 @@ async function publishCompletedReview(
471
482
  approveMaxPriorityLevel: record.invocation.approveMaxPriorityLevel,
472
483
  expectedRepository: record.repository,
473
484
  review: record.review,
474
- ...(useDegradedPublicationBody ? { publicationBody: record.publicationBody } : {}),
475
- // Fully parsed reviews publish only the concise host-rendered body plus
476
- // inline findings. If that payload cannot fit, publication fails closed;
477
- // retained raw synthesis is never substituted as a public report dump.
478
- // A degraded synthesis stays COMMENT-only, but its safely parsed findings
479
- // still earn inline placement; only unparsable output is forced body-only.
485
+ ...(publicationNotice ? { publicationNotice } : {}),
486
+ // Every review publishes only the concise host-rendered body plus validated
487
+ // inline findings. Retained raw synthesis and lane evidence remain private
488
+ // completion diagnostics, including for degraded/incomplete COMMENT runs.
489
+ // If the concise payload cannot fit, publication fails closed.
480
490
  forceBodyOnly: record.synthesisQuality !== undefined &&
481
491
  record.synthesisQuality !== "fully_parsed" &&
482
492
  !(Array.isArray(record.review.findings) && record.review.findings.length > 0),
483
493
  // A publication body identifies Markdown-derived or degraded synthesis.
484
494
  // Enforce COMMENT again at the final publication boundary so restored
485
495
  // canonical artifacts created by an older parser cannot inherit APPROVE.
486
- forceComment: record.mergeApprovalEligible === false || useDegradedPublicationBody ||
487
- (record.synthesisQuality !== undefined &&
488
- (record.synthesisQuality !== "fully_parsed" || record.completeness === "incomplete")),
496
+ forceComment: record.mergeApprovalEligible === false || degradedPublication,
489
497
  });
490
498
  notifyPublishResult(result, source, ctx);
491
499
  return result;
@@ -384,14 +384,18 @@ function parseFindings(text: string): { findings: ReviewFindingLike[]; count: nu
384
384
  const severity = explicitSeverity?.toLowerCase() === "nit" ? "nit" : explicitSeverity?.toUpperCase();
385
385
  const normalizedTag = tagged?.toLowerCase() === "nit" ? "nit" : tagged?.toUpperCase();
386
386
  const rationale = field(block, "Rationale") ?? field(block, "Why");
387
+ const confidenceText = field(block, "Confidence");
388
+ const confidence = confidenceText === undefined ? undefined : Number(confidenceText.trim());
389
+ const validConfidence = confidenceText !== undefined &&
390
+ /^(?:0(?:\.\d+)?|1(?:\.0+)?)$/.test(confidenceText.trim()) && Number.isFinite(confidence);
387
391
  const location = parseLocation(field(block, "Location"));
388
392
  const recognizedFieldCount = fieldCount(block, "Severity") + fieldCount(block, "Rationale") +
389
- fieldCount(block, "Why") + fieldCount(block, "Location");
390
- const unconsumed = block.replace(/^\*\*(?:Severity|Rationale|Why|Location):\*\*.*$/gim, "").trim();
393
+ fieldCount(block, "Why") + fieldCount(block, "Confidence") + fieldCount(block, "Location");
394
+ const unconsumed = block.replace(/^\*\*(?:Severity|Rationale|Why|Confidence|Location):\*\*.*$/gim, "").trim();
391
395
  if (
392
- !explicitSeverity || !rationale?.trim() || recognizedFieldCount < 2 || unconsumed ||
396
+ !explicitSeverity || !rationale?.trim() || recognizedFieldCount < 2 || unconsumed || !validConfidence ||
393
397
  fieldCount(block, "Severity") !== 1 || fieldCount(block, "Rationale") + fieldCount(block, "Why") !== 1 ||
394
- fieldCount(block, "Location") > 1
398
+ fieldCount(block, "Confidence") !== 1 || fieldCount(block, "Location") > 1
395
399
  ) {
396
400
  complete = false;
397
401
  continue;
@@ -410,7 +414,7 @@ function parseFindings(text: string): { findings: ReviewFindingLike[]; count: nu
410
414
  severity,
411
415
  blocking: severity === "P0" || severity === "P1",
412
416
  body: rationale,
413
- confidence_score: 1,
417
+ confidence_score: confidence,
414
418
  code_location: location.location,
415
419
  });
416
420
  }
@@ -470,7 +474,6 @@ function syntheticReview(
470
474
  verdict,
471
475
  overall_correctness: verdict === "request_changes" ? "patch is incorrect" : "patch is correct",
472
476
  overall_explanation: overview.split(/\n\n|\n/)[0] ?? "Review completed.",
473
- overall_confidence_score: 1,
474
477
  };
475
478
  }
476
479
 
@@ -840,9 +843,9 @@ export function synthesizeReviewArtifact(input: {
840
843
  }
841
844
  return reasons.length > 0 ? reasons : ["terminal synthesis was not structurally parseable; preserved as body-only Markdown"];
842
845
  })();
843
- // Markdown is the durable semantic product. A fully parsed complete synthesis
844
- // publishes verbatim; every degraded synthesis publishes the deterministic
845
- // host-rendered body so labels stay readable while all content is retained.
846
+ // Markdown is the durable semantic product. Keep the complete deterministic
847
+ // body for local rendering, cache diagnostics, and extraction. GitHub
848
+ // publication independently renders a concise host summary for every quality.
846
849
  const body = quality === "fully_parsed"
847
850
  ? safeReviewBody(raw)
848
851
  : buildDegradedReviewBody({
@@ -596,7 +596,7 @@ export interface CompletedReviewSessionIdentity {
596
596
  }
597
597
 
598
598
  export interface PersistedCompletedReview {
599
- schemaVersion: 2;
599
+ schemaVersion: 3;
600
600
  session: CompletedReviewSessionIdentity;
601
601
  invocation: ReviewInvocation;
602
602
  repository: RepositoryBinding;
@@ -819,7 +819,7 @@ export class CompletedReviewCache {
819
819
  const digest = reviewHash(record.review);
820
820
  const useReference = !!reviewEntryId && !!referencedReview && reviewHash(referencedReview) === digest;
821
821
  return {
822
- schemaVersion: 2,
822
+ schemaVersion: 3,
823
823
  session: { ...session },
824
824
  invocation: { ...record.invocation, autoPost: { ...record.invocation.autoPost } },
825
825
  repository: { ...record.repository },
@@ -849,7 +849,7 @@ export class CompletedReviewCache {
849
849
  ): boolean {
850
850
  if (
851
851
  !isObject(value) ||
852
- value.schemaVersion !== 2 ||
852
+ value.schemaVersion !== 3 ||
853
853
  !validSessionIdentity(value.session) ||
854
854
  !sameSessionIdentity(value.session, session) ||
855
855
  !validRepositoryBinding(value.repository)
@@ -866,7 +866,7 @@ export class CompletedReviewCache {
866
866
  const candidate = hasReference ? referencedReview : value.review;
867
867
  let parsed: PublishableReviewParseResult;
868
868
  try {
869
- parsed = parsePublishableReview(JSON.stringify(candidate));
869
+ parsed = canonicalReviewSnapshot(candidate as ReviewLike);
870
870
  } catch {
871
871
  return false;
872
872
  }
@@ -1083,7 +1083,10 @@ function publishableReviewEnvelope(text: string): { text: string; source: "json"
1083
1083
  * Publication accepts one complete JSON object. A single surrounding Markdown code
1084
1084
  * fence is tolerated for compatibility but remains distinguishable from exact JSON.
1085
1085
  */
1086
- export function parsePublishableReview(text: string): PublishableReviewParseResult {
1086
+ export function parsePublishableReview(
1087
+ text: string,
1088
+ options: { allowMissingOverallConfidence?: boolean } = {},
1089
+ ): PublishableReviewParseResult {
1087
1090
  const envelope = publishableReviewEnvelope(text);
1088
1091
  let value: unknown;
1089
1092
  try {
@@ -1140,7 +1143,11 @@ export function parsePublishableReview(text: string): PublishableReviewParseResu
1140
1143
  if (!new Set(["patch is correct", "patch is incorrect"]).has(String(value.overall_correctness))) {
1141
1144
  return { error: "overall_correctness is invalid" };
1142
1145
  }
1143
- if (!isConfidence(value.overall_confidence_score)) {
1146
+ if (
1147
+ value.overall_confidence_score !== undefined
1148
+ ? !isConfidence(value.overall_confidence_score)
1149
+ : !options.allowMissingOverallConfidence
1150
+ ) {
1144
1151
  return { error: "overall_confidence_score is invalid" };
1145
1152
  }
1146
1153
  return { review: value as unknown as ReviewLike, source: envelope.source };
@@ -1257,7 +1264,11 @@ function findingAnchor(finding: ReviewFindingLike): string | undefined {
1257
1264
  return `${path}:${side}:${start}:${end}`;
1258
1265
  }
1259
1266
 
1260
- export function buildReviewSummary(review: ReviewLike, inlineComments: PublishComment[] = []): string {
1267
+ export function buildReviewSummary(
1268
+ review: ReviewLike,
1269
+ inlineComments: PublishComment[] = [],
1270
+ publicationNotice?: string,
1271
+ ): string {
1261
1272
  const findings = Array.isArray(review.findings) ? review.findings : [];
1262
1273
  const inlineAnchors = new Map<string, number>();
1263
1274
  for (const comment of inlineComments) {
@@ -1281,6 +1292,7 @@ export function buildReviewSummary(review: ReviewLike, inlineComments: PublishCo
1281
1292
  ? "Approve"
1282
1293
  : "Comment";
1283
1294
  const lines = [`**Verdict:** ${verdict}`];
1295
+ if (publicationNotice?.trim()) lines.push("", publicationNotice.trim());
1284
1296
  if (inlineComments.length > 0) {
1285
1297
  lines.push("", "See the inline review comments for the primary findings.");
1286
1298
  }
@@ -1416,7 +1428,10 @@ export function canonicalReviewSnapshot(review: ReviewLike): PublishableReviewPa
1416
1428
  if (typeof serialized !== "string") {
1417
1429
  return { error: "review could not be serialized for publication" };
1418
1430
  }
1419
- return parsePublishableReview(serialized);
1431
+ // Host-synthesized canonical Markdown has no manufactured overall score,
1432
+ // but every parsed finding still requires its validated numeric confidence.
1433
+ // Assistant-authored strict JSON requires both at its public parse boundary.
1434
+ return parsePublishableReview(serialized, { allowMissingOverallConfidence: true });
1420
1435
  }
1421
1436
 
1422
1437
  export function validateInlineComments(
@@ -1503,6 +1518,7 @@ function buildLosslessReviewPayload(input: {
1503
1518
  changedFiles?: readonly ChangedFileLike[];
1504
1519
  bodyPreamble?: string;
1505
1520
  bodyOverride?: string;
1521
+ publicationNotice?: string;
1506
1522
  diagnostics?: readonly string[];
1507
1523
  event?: ReviewEventType;
1508
1524
  }): { payload?: PullReviewPayload; diagnostics: string[]; errors: string[] } {
@@ -1520,6 +1536,9 @@ function buildLosslessReviewPayload(input: {
1520
1536
  if (input.bodyPreamble && containsReservedReviewMarker(input.bodyPreamble)) {
1521
1537
  return { diagnostics, errors: ["publication preamble contains a reserved pi-pr-review marker"] };
1522
1538
  }
1539
+ if (input.publicationNotice && containsReservedReviewMarker(input.publicationNotice)) {
1540
+ return { diagnostics, errors: ["publication notice contains a reserved pi-pr-review marker"] };
1541
+ }
1523
1542
  const selected = input.allowInlineComments
1524
1543
  ? selectInlineComments(input.review, input.changedFiles ?? [])
1525
1544
  : { comments: [], diagnostics: [], errors: [] };
@@ -1529,7 +1548,10 @@ function buildLosslessReviewPayload(input: {
1529
1548
  // Inline-placement diagnostics remain available to the host notification,
1530
1549
  // while every affected finding is retained under Other Notes. Do not expose
1531
1550
  // transport diagnostics as if they were review findings.
1532
- let content = input.bodyOverride?.trim() || buildReviewSummary(input.review, selected.comments);
1551
+ let content = input.bodyOverride?.trim() || buildReviewSummary(input.review, selected.comments, input.publicationNotice);
1552
+ if (input.bodyOverride?.trim() && input.publicationNotice?.trim()) {
1553
+ content = `${content}\n\n${input.publicationNotice.trim()}`;
1554
+ }
1533
1555
  if (input.bodyPreamble?.trim()) content = `${input.bodyPreamble.trim()}\n\n${content}`;
1534
1556
  const marker = canonicalReviewMarker(markerHeadSha);
1535
1557
  const bodyError = validateReviewBody(content);
@@ -2148,8 +2170,10 @@ export async function publishPullReview(input: {
2148
2170
  approveMaxPriorityLevel?: ApproveMaxPriorityLevel;
2149
2171
  expectedRepository?: RepositoryBinding;
2150
2172
  review: ReviewLike;
2151
- /** Host-sanitized original synthesis retained when structured extraction is partial or absent. */
2173
+ /** Compatibility-only body override. The extension lifecycle keeps retained synthesis private. */
2152
2174
  publicationBody?: string;
2175
+ /** Short host-owned warning included in the concise summary. */
2176
+ publicationNotice?: string;
2153
2177
  /** Prevent uncertain/raw synthesis from selecting APPROVE or inline anchors. */
2154
2178
  forceBodyOnly?: boolean;
2155
2179
  /** Prevent partially trusted synthesis from selecting a merge-relevant event. */
@@ -2166,6 +2190,7 @@ export async function publishPullReview(input: {
2166
2190
  expectedRepository,
2167
2191
  review,
2168
2192
  publicationBody,
2193
+ publicationNotice,
2169
2194
  forceBodyOnly = false,
2170
2195
  forceComment = false,
2171
2196
  } = input;
@@ -2266,6 +2291,7 @@ export async function publishPullReview(input: {
2266
2291
  allowInlineComments,
2267
2292
  changedFiles,
2268
2293
  ...(publicationBody ? { bodyOverride: publicationBody } : {}),
2294
+ ...(publicationNotice ? { publicationNotice } : {}),
2269
2295
  ...(isApprove ? { event: APPROVE_EVENT } : {}),
2270
2296
  ...(changedFileLookupFailed ? { diagnostics: [CHANGED_FILE_LOOKUP_DIAGNOSTIC] } : {}),
2271
2297
  ...(headPlan.stale
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pr-review",
3
- "version": "1.17.1",
3
+ "version": "1.17.3",
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",
@@ -222,6 +222,7 @@ Return Markdown using these stable headings. Do not emit a GitHub API payload an
222
222
  ### [P1] Imperative finding title
223
223
  **Severity:** P1
224
224
  **Rationale:** Concise Markdown explaining the trigger, impact, and source-grounded evidence.
225
+ **Confidence:** 0.92
225
226
  **Location:** `path/to/file.ts:10-12 RIGHT`
226
227
 
227
228
  ## Lane completeness
@@ -231,6 +232,6 @@ Return Markdown using these stable headings. Do not emit a GitHub API payload an
231
232
  <useful strengths plus concise correctness, security, or performance notes>
232
233
  ```
233
234
 
234
- Repeat the `###` finding block for every confirmed finding. Omit `Location` when no safe diff location exists. Use `No findings.` under `## Findings` when empty. The extension retains the complete raw synthesis even when some or all finding blocks cannot be parsed. Canonical heading discovery ignores heading-like content in CommonMark fenced-code and HTML-block contexts. It validates safe P0–P3 anchors for inline placement, preserves ambiguous or unparsed substantive content in exactly one sanitized body-only `COMMENT`, and degrades malformed synthesis the same way; if synthesis is absent, it assembles a deterministic body-only review from retained lane artifacts. Optional formatting repair can never block this fallback.
235
+ Repeat the `###` finding block for every confirmed finding. `Confidence` is the independently validated numeric confidence for that finding from 0.0 through 1.0; do not use a fixed default. Omit `Location` when no safe diff location exists. Use `No findings.` under `## Findings` when empty. The extension retains the complete raw synthesis even when some or all finding blocks cannot be parsed. Canonical heading discovery ignores heading-like content in CommonMark fenced-code and HTML-block contexts. It validates safe P0–P3 anchors for inline placement, preserves ambiguous or unparsed substantive content in exactly one sanitized body-only `COMMENT`, and degrades malformed synthesis the same way; if synthesis is absent, it assembles a deterministic body-only review from retained lane artifacts. Optional formatting repair can never block this fallback.
235
236
 
236
237
  Host code captures repository, PR, reviewed head, lifecycle, posting authority, stale policy, and invocation identity before review execution, serializes the single GitHub request with `JSON.stringify`, sanitizes reserved markers, enforces payload limits, appends the canonical marker, and performs duplicate/stale/draft/lifecycle/final-head checks. The assistant's semantic `approve` is only one input to the host gates; assistant text cannot directly choose `APPROVE`, `REQUEST_CHANGES`, commit ID, API path, repository, or hostname. Parent candidate validation must gather all independent evidence in one wave and use at most one dependency-driven follow-up, as specified in Step 7.