pi-pr-review 1.12.2 → 1.12.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 +7 -0
- package/README.md +2 -2
- package/lib/pr-review-markdown.ts +53 -17
- package/package.json +1 -1
- package/prompts/pr-review.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.12.3](https://github.com/10ego/pi-pr-review/compare/v1.12.2...v1.12.3) (2026-08-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **review:** make host lane truth authoritative for completeness ([#67](https://github.com/10ego/pi-pr-review/issues/67)) ([ad70fc3](https://github.com/10ego/pi-pr-review/commit/ad70fc3b6376f215bf045b980fef06bd87036bd1))
|
|
9
|
+
|
|
3
10
|
## [1.12.2](https://github.com/10ego/pi-pr-review/compare/v1.12.1...v1.12.2) (2026-08-19)
|
|
4
11
|
|
|
5
12
|
|
package/README.md
CHANGED
|
@@ -176,7 +176,7 @@ Every invocation has a host-owned monotonic 15-minute hard cap, including the tw
|
|
|
176
176
|
|
|
177
177
|
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.
|
|
178
178
|
|
|
179
|
-
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 lens/shard, 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.
|
|
179
|
+
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 lens/shard, 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.
|
|
180
180
|
|
|
181
181
|
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 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.
|
|
182
182
|
|
|
@@ -212,7 +212,7 @@ Review semantics are Markdown-first. A deterministic tolerant parser normalizes
|
|
|
212
212
|
|
|
213
213
|
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.
|
|
214
214
|
|
|
215
|
-
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
|
|
215
|
+
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 body-only `COMMENT` reviews. 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; if the concise body plus its canonical marker exceeds GitHub's limit, the host uses its sanitized, size-bounded Markdown projection instead of dropping the review. Stale or authorized non-open reviews are body-only. A stale approval additionally requires `allowStaleApprovals: true`.
|
|
216
216
|
|
|
217
217
|
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. Raw and lane-assembled fallbacks are body-only and `COMMENT`-only; assistant text cannot select event, commit, repository, hostname, API path, or inline anchors. Unknown or invalid host states fail closed before a write.
|
|
218
218
|
|
|
@@ -654,16 +654,37 @@ export function synthesizeReviewArtifact(input: {
|
|
|
654
654
|
const verification = section(raw, "Verification");
|
|
655
655
|
const laneDisclosure = section(raw, "Lane completeness");
|
|
656
656
|
const laneDisclosureClaimsComplete = /^all requested lanes completed\.?$/i.test(laneDisclosure?.trim() ?? "");
|
|
657
|
+
const exactLaneCoverage = expectedLaneCount > 0 && lanes.length === expectedLaneCount &&
|
|
658
|
+
new Set(lanes.map((lane) => lane.key)).size === expectedLaneCount &&
|
|
659
|
+
new Set(expectedLaneDescriptors.map((lane) => lane.key)).size === expectedLaneCount &&
|
|
660
|
+
lanes.every((lane) => expectedLaneDescriptors.some((expected) =>
|
|
661
|
+
expected.key === lane.key && expected.tier === lane.tier &&
|
|
662
|
+
expected.minorHygiene === !!lane.minorHygiene));
|
|
663
|
+
// Host lane artifacts are authoritative whenever a batch ran: they already
|
|
664
|
+
// stop a false complete claim from upgrading incomplete lanes, and they must
|
|
665
|
+
// equally stop a paraphrased or omitted disclosure line from downgrading a
|
|
666
|
+
// host-complete batch to body-only publication. Completeness additionally
|
|
667
|
+
// requires the retained lanes to cover every expected dispatch, so an
|
|
668
|
+
// expected-but-unretained artifact cannot make an incomplete batch look
|
|
669
|
+
// complete through vacuous satisfaction.
|
|
670
|
+
const batchEvidencePresent = lanes.length > 0 || expectedLaneCount > 0;
|
|
671
|
+
const laneTruthClaimsComplete = batchEvidencePresent
|
|
672
|
+
? lanes.every((lane) => lane.lifecycle === "complete") &&
|
|
673
|
+
(expectedLaneCount === 0 || exactLaneCoverage)
|
|
674
|
+
: laneDisclosureClaimsComplete;
|
|
657
675
|
const preamble = documentPreamble(raw);
|
|
658
676
|
const verdictField = field(preamble, "Verdict");
|
|
659
677
|
const verdict = verdictField?.toLowerCase().replace(/[ -]+/g, "_");
|
|
660
|
-
|
|
661
|
-
|
|
678
|
+
// A missing section is a structural gap handled by hasStructure and
|
|
679
|
+
// diagnostics; only present-but-unsafe text disables inline extraction.
|
|
680
|
+
const safeIfPresent = (value: string | undefined) => value === undefined || publicationSafeText(value);
|
|
681
|
+
const extractedControlsSafe = safeIfPresent(overview) && safeIfPresent(verification) &&
|
|
682
|
+
safeIfPresent(laneDisclosure) && safeIfPresent(verdictField) &&
|
|
662
683
|
new Set(["approve", "request_changes", "comment"]).has(verdict ?? "") &&
|
|
663
684
|
fieldCount(preamble, "Verdict") === 1 && fieldCount(raw, "Verdict") === 1;
|
|
664
685
|
const canonicalParsed = parsed.unsafe ? parsed : { ...parsed, unsafe: !extractedControlsSafe };
|
|
665
|
-
const completeness = synthesisCompleteness(raw, lanes,
|
|
666
|
-
const hasStructure = !!overview && !!verification &&
|
|
686
|
+
const completeness = synthesisCompleteness(raw, lanes, laneTruthClaimsComplete);
|
|
687
|
+
const hasStructure = !!overview && !!verification && laneTruthClaimsComplete && extractedControlsSafe;
|
|
667
688
|
const quality: ReviewSynthesisQuality = canonicalParsed.unsafe
|
|
668
689
|
? "raw"
|
|
669
690
|
: hasStructure && canonicalParsed.complete && completeness === "complete"
|
|
@@ -675,12 +696,6 @@ export function synthesizeReviewArtifact(input: {
|
|
|
675
696
|
const bodySource = quality === "fully_parsed" ? raw : synthesisWithRetainedLaneEvidence(raw, lanes);
|
|
676
697
|
const body = safeReviewBodyWithLaneDisclosure(bodySource, lanes);
|
|
677
698
|
const safeFindings = canonicalParsed.unsafe ? [] : canonicalParsed.findings;
|
|
678
|
-
const exactLaneCoverage = expectedLaneCount > 0 && lanes.length === expectedLaneCount &&
|
|
679
|
-
new Set(lanes.map((lane) => lane.key)).size === expectedLaneCount &&
|
|
680
|
-
new Set(expectedLaneDescriptors.map((lane) => lane.key)).size === expectedLaneCount &&
|
|
681
|
-
lanes.every((lane) => expectedLaneDescriptors.some((expected) =>
|
|
682
|
-
expected.key === lane.key && expected.tier === lane.tier &&
|
|
683
|
-
expected.minorHygiene === !!lane.minorHygiene));
|
|
684
699
|
return Object.freeze({
|
|
685
700
|
quality,
|
|
686
701
|
rawText: input.rawText,
|
|
@@ -701,12 +716,33 @@ export function synthesizeReviewArtifact(input: {
|
|
|
701
716
|
// Markdown approval requires exact host evidence for every registered
|
|
702
717
|
// dispatch; a nonempty subset cannot establish requested coverage.
|
|
703
718
|
mergeApprovalEligible: quality === "fully_parsed" && completeness === "complete" && exactLaneCoverage,
|
|
704
|
-
diagnostics: Object.freeze(
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
719
|
+
diagnostics: Object.freeze((() => {
|
|
720
|
+
if (canonicalParsed.unsafe) {
|
|
721
|
+
return ["unsafe Markdown fields were preserved in the sanitized body and inline extraction was disabled"];
|
|
722
|
+
}
|
|
723
|
+
if (quality === "fully_parsed") return [];
|
|
724
|
+
const reasons: string[] = [];
|
|
725
|
+
if (!overview) reasons.push("Overview section missing or empty");
|
|
726
|
+
if (!verification) reasons.push("Verification section missing or empty");
|
|
727
|
+
if (!laneTruthClaimsComplete) {
|
|
728
|
+
if (lanes.length > 0 && !lanes.every((lane) => lane.lifecycle === "complete")) {
|
|
729
|
+
reasons.push("host lane evidence contains incomplete lanes");
|
|
730
|
+
} else if (expectedLaneCount > 0 && !exactLaneCoverage) {
|
|
731
|
+
reasons.push("retained lane evidence does not cover every expected lane dispatch");
|
|
732
|
+
} else {
|
|
733
|
+
reasons.push("Lane completeness section absent or did not state the canonical completion line");
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (verdictField !== undefined && !new Set(["approve", "request_changes", "comment"]).has(verdict ?? "")) {
|
|
737
|
+
reasons.push("Verdict field outside the canonical set");
|
|
738
|
+
}
|
|
739
|
+
if (fieldCount(preamble, "Verdict") !== 1 || fieldCount(raw, "Verdict") !== 1) {
|
|
740
|
+
reasons.push("Verdict field count is not exactly one");
|
|
741
|
+
}
|
|
742
|
+
if (parsed.count > parsed.findings.length) {
|
|
743
|
+
reasons.push(`${parsed.count - parsed.findings.length} finding section(s) could not be parsed and remain in the body`);
|
|
744
|
+
}
|
|
745
|
+
return reasons.length > 0 ? reasons : ["terminal synthesis was not structurally parseable; preserved as body-only Markdown"];
|
|
746
|
+
})()),
|
|
711
747
|
});
|
|
712
748
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pr-review",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.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",
|
package/prompts/pr-review.md
CHANGED
|
@@ -196,7 +196,7 @@ The orchestrator must never call `gh` to post comments or reviews. Always finish
|
|
|
196
196
|
|
|
197
197
|
After host synthesis, the extension caches one validated completed review as a canonical host-owned 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. `/pr-review-publish` and a matching direct request publish only the cache and never start or rerun review agents. On a later turn, the extension intercepts that direct input before an agent turn and permits stale publication without asking the orchestrator to recreate the review.
|
|
198
198
|
|
|
199
|
-
Every authorized publish path builds one GitHub review payload and sends at most one review `POST`; the extension never submits `REQUEST_CHANGES`. Fully parsed Markdown with one complete retained artifact for every host-registered dispatch
|
|
199
|
+
Every authorized publish path builds one GitHub review payload and sends at most one review `POST`; the extension never submits `REQUEST_CHANGES`. Fully parsed Markdown with one complete retained artifact for every host-registered dispatch and retained strict host-bound JSON share the same gated `APPROVE` path. 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, and partial, malformed, unsafe, or lane-fallback Markdown remain body-only `COMMENT`. Restored cache entries must reclassify retained lane output, reproduce the exact host-recorded artifact key set, and bind every complete artifact to the frozen invocation generation, tier, and minor-hygiene contract and re-synthesize the retained raw text under that frozen binding before preserving approval eligibility. For a current, open PR, the first 50 eligible P0–P3 findings with valid, unique diff anchors are inline. The public body contains the verdict, an inline-review cue when applicable, and `Other Notes` for nits and every finding that cannot be inline; overview, verification, strengths, and transport diagnostics remain only in the retained internal artifact. If that concise body plus its canonical marker exceeds GitHub's limit, the host publishes the sanitized, size-bounded original Markdown projection instead of dropping the review. Stale reviews and authorized closed or merged reviews are body-only. A stale review may record a qualified `APPROVE` only with the separate trusted `allowStaleApprovals: true` opt-in captured before review execution. A failed write never triggers a fallback POST.
|
|
200
200
|
|
|
201
201
|
Every path retains the same safety gates: captured posting authority, exact repository/PR/review binding, safe locations, no reserved review markers, bounded bodies and payloads, current-head and stale policy, draft and lifecycle checks, non-open authorization, same-head duplicate detection, and a final head check. Unknown lifecycle states and unconfirmed non-open writes fail closed. The session-backed cache survives extension reloads and session resumes but remains bound to the originating session instance and repository. If the captured stale setting disabled publication, the user may explicitly run `/pr-review-publish <PR-NUM> --allow-stale`. Never rerun the review merely to change posting intent, and never attempt a direct GitHub write yourself.
|
|
202
202
|
|