pi-pr-review 1.11.0 → 1.11.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/README.md +1 -1
- package/extensions/pr-review-focus.ts +14 -1
- package/extensions/pr-review-subagent.ts +26 -1
- package/extensions/review-table.ts +96 -31
- package/lib/pr-review-loop.ts +18 -0
- package/lib/pr-review-publish.ts +24 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.11.2](https://github.com/10ego/pi-pr-review/compare/v1.11.1...v1.11.2) (2026-07-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **publish:** repair malformed reviews with light subagent ([#42](https://github.com/10ego/pi-pr-review/issues/42)) ([6bb2f1b](https://github.com/10ego/pi-pr-review/commit/6bb2f1b5d7c42ab0233be27b7fa75984eb0b6357))
|
|
9
|
+
|
|
10
|
+
## [1.11.1](https://github.com/10ego/pi-pr-review/compare/v1.11.0...v1.11.1) (2026-07-20)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **focus:** coalesce live viewer redraws ([#40](https://github.com/10ego/pi-pr-review/issues/40)) ([6553eb1](https://github.com/10ego/pi-pr-review/commit/6553eb1c0ade7881b220269b22adf22e4a5d7e59))
|
|
16
|
+
|
|
3
17
|
## [1.11.0](https://github.com/10ego/pi-pr-review/compare/v1.10.7...v1.11.0) (2026-07-17)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -188,7 +188,7 @@ Publishing is off by default.
|
|
|
188
188
|
|
|
189
189
|
The extension—not the model—owns publishing. After a successful review, it caches one validated completed review 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.
|
|
190
190
|
|
|
191
|
-
If the agent's final review is not valid exact-contract JSON, the extension logs the reason and
|
|
191
|
+
If the agent's final review is not valid exact-contract JSON, the extension logs the reason and runs the configured `light` subagent once to reformat the completed output, with tools disabled and the original posting authority unchanged. An invalid correction or overlapping input stops publication rather than starting another correction loop.
|
|
192
192
|
|
|
193
193
|
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.
|
|
194
194
|
|
|
@@ -47,6 +47,7 @@ export class ReviewFocusView implements Component {
|
|
|
47
47
|
private followTail = true;
|
|
48
48
|
private unsubscribe?: () => void;
|
|
49
49
|
private closed = false;
|
|
50
|
+
private renderTimer?: ReturnType<typeof setTimeout>;
|
|
50
51
|
|
|
51
52
|
constructor(
|
|
52
53
|
private readonly tui: Pick<TUI, "requestRender" | "terminal">,
|
|
@@ -74,7 +75,7 @@ export class ReviewFocusView implements Component {
|
|
|
74
75
|
} else {
|
|
75
76
|
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, snapshot.passes.length - 1));
|
|
76
77
|
}
|
|
77
|
-
this.
|
|
78
|
+
this.scheduleRender();
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
handleInput(data: string): void {
|
|
@@ -199,6 +200,18 @@ export class ReviewFocusView implements Component {
|
|
|
199
200
|
dispose(): void {
|
|
200
201
|
this.unsubscribe?.();
|
|
201
202
|
this.unsubscribe = undefined;
|
|
203
|
+
if (this.renderTimer !== undefined) {
|
|
204
|
+
clearTimeout(this.renderTimer);
|
|
205
|
+
this.renderTimer = undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private scheduleRender(): void {
|
|
210
|
+
if (this.closed || this.renderTimer !== undefined) return;
|
|
211
|
+
this.renderTimer = setTimeout(() => {
|
|
212
|
+
this.renderTimer = undefined;
|
|
213
|
+
if (!this.closed) this.tui.requestRender();
|
|
214
|
+
}, 16);
|
|
202
215
|
}
|
|
203
216
|
|
|
204
217
|
private pageSize(): number {
|
|
@@ -608,6 +608,7 @@ interface SubagentPassRequest {
|
|
|
608
608
|
toolPolicy?: ToolPolicy;
|
|
609
609
|
majorOnly?: boolean;
|
|
610
610
|
minorHygiene?: boolean;
|
|
611
|
+
systemPrompt?: string;
|
|
611
612
|
focusPublisher?: ReviewFocusPublisher;
|
|
612
613
|
}
|
|
613
614
|
|
|
@@ -704,7 +705,7 @@ async function runSubagentAttempt(
|
|
|
704
705
|
|
|
705
706
|
tmp = await writeTempPrompt(
|
|
706
707
|
pass.tier,
|
|
707
|
-
buildSubagentSystemPrompt(
|
|
708
|
+
pass.systemPrompt ?? buildSubagentSystemPrompt(
|
|
708
709
|
pass.tier,
|
|
709
710
|
pass.majorOnly === true,
|
|
710
711
|
pass.minorHygiene && pass.tier === "light",
|
|
@@ -812,6 +813,30 @@ async function runSelfReviewAttempt(
|
|
|
812
813
|
}
|
|
813
814
|
}
|
|
814
815
|
|
|
816
|
+
const OUTPUT_REPAIR_SYSTEM_PROMPT = [
|
|
817
|
+
"You are an isolated output-repair subagent for a completed PR review.",
|
|
818
|
+
"You have no tools. Do not review the PR, add findings, remove findings, or change the review's substance.",
|
|
819
|
+
"Convert the supplied completed-review text into exactly one JSON object that satisfies the supplied output contract.",
|
|
820
|
+
"Return only that JSON object: no Markdown, headings, prose, or code fences. If the source cannot be safely converted, return it unchanged so host validation rejects it.",
|
|
821
|
+
].join("\\n");
|
|
822
|
+
|
|
823
|
+
/** Run the configured light-tier model as a no-tools, one-shot JSON-format repairer. */
|
|
824
|
+
export async function repairReviewOutput(
|
|
825
|
+
text: string,
|
|
826
|
+
outputContract: string,
|
|
827
|
+
ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">,
|
|
828
|
+
signal?: AbortSignal,
|
|
829
|
+
): Promise<string | undefined> {
|
|
830
|
+
const result = await runSubagentPass(loadConfig(ctx), ctx, {
|
|
831
|
+
id: "output-repair",
|
|
832
|
+
tier: "light",
|
|
833
|
+
objective: `Reformat this completed review without changing its substance.\n\n--- required output contract ---\n${outputContract}\n\n--- completed review ---\n${text}`,
|
|
834
|
+
toolPolicy: "none",
|
|
835
|
+
systemPrompt: OUTPUT_REPAIR_SYSTEM_PROMPT,
|
|
836
|
+
}, signal);
|
|
837
|
+
return result.status === "completed" && result.text.trim() ? result.text : undefined;
|
|
838
|
+
}
|
|
839
|
+
|
|
815
840
|
async function runSubagentPass(
|
|
816
841
|
config: PrReviewConfig,
|
|
817
842
|
ctx: Pick<ExtensionContext, "cwd">,
|
|
@@ -86,10 +86,23 @@ interface Review {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
type MessagePart = { type: string; text?: string };
|
|
89
|
+
type ReviewOutputRepair = (
|
|
90
|
+
text: string,
|
|
91
|
+
outputContract: string,
|
|
92
|
+
ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">,
|
|
93
|
+
signal?: AbortSignal,
|
|
94
|
+
) => Promise<string | undefined>;
|
|
95
|
+
|
|
96
|
+
const defaultReviewOutputRepair: ReviewOutputRepair = async (...args) => {
|
|
97
|
+
const { repairReviewOutput } = await import("./pr-review-subagent.ts");
|
|
98
|
+
return repairReviewOutput(...args);
|
|
99
|
+
};
|
|
89
100
|
|
|
90
101
|
const OWN_REVIEW_PROMPT = fs.realpathSync(
|
|
91
102
|
fileURLToPath(new URL("../prompts/pr-review.md", import.meta.url)),
|
|
92
103
|
);
|
|
104
|
+
const REVIEW_PROMPT_TEXT = fs.readFileSync(OWN_REVIEW_PROMPT, "utf8");
|
|
105
|
+
const REVIEW_OUTPUT_CONTRACT = REVIEW_PROMPT_TEXT.slice(REVIEW_PROMPT_TEXT.indexOf("## OUTPUT FORMAT"));
|
|
93
106
|
|
|
94
107
|
function isOwnReviewPrompt(pi: Pick<ExtensionAPI, "getCommands">): boolean {
|
|
95
108
|
try {
|
|
@@ -436,6 +449,7 @@ export default function registerReviewTable(
|
|
|
436
449
|
pi: ExtensionAPI,
|
|
437
450
|
loopCoordinator = new ReviewLoopCoordinator(pi),
|
|
438
451
|
selfReviewCoordinator = new SelfReviewPermitCoordinator(pi, () => !!loopCoordinator.peek()),
|
|
452
|
+
repairOutput: ReviewOutputRepair = defaultReviewOutputRepair,
|
|
439
453
|
) {
|
|
440
454
|
const completedReviews = new CompletedReviewCache();
|
|
441
455
|
const sessionIdentity = (ctx: ExtensionContext): CompletedReviewSessionIdentity | undefined => {
|
|
@@ -454,7 +468,11 @@ export default function registerReviewTable(
|
|
|
454
468
|
restoreCompletedReviewBranch(completedReviews, ctx.sessionManager.getBranch(), session);
|
|
455
469
|
};
|
|
456
470
|
type PendingCompletion =
|
|
457
|
-
| {
|
|
471
|
+
| {
|
|
472
|
+
readonly record: CompletedReviewRecord;
|
|
473
|
+
readonly replacedRecord?: CompletedReviewRecord;
|
|
474
|
+
readonly session?: CompletedReviewSessionIdentity;
|
|
475
|
+
}
|
|
458
476
|
| { readonly error: string };
|
|
459
477
|
let pendingCompletion: PendingCompletion | undefined;
|
|
460
478
|
const completionError = (invocation: ReviewInvocation, failure?: string): PendingCompletion | undefined => {
|
|
@@ -474,12 +492,15 @@ export default function registerReviewTable(
|
|
|
474
492
|
try {
|
|
475
493
|
const repository = await resolveRepositoryBinding(ctx.cwd);
|
|
476
494
|
// Cache before publication preflight; persist after Pi stores the assistant message.
|
|
477
|
-
const
|
|
495
|
+
const replacement = completedReviews.replace(parsed.review, invocation, repository);
|
|
496
|
+
const { record } = replacement;
|
|
478
497
|
const session = sessionIdentity(ctx);
|
|
479
498
|
if (!session) {
|
|
480
499
|
ctx.ui.notify("Completed review cache will not survive reload: session identity is unavailable", "warning");
|
|
481
500
|
}
|
|
482
|
-
return session
|
|
501
|
+
return session
|
|
502
|
+
? { record, ...(replacement.previous ? { replacedRecord: replacement.previous } : {}), session }
|
|
503
|
+
: { record, ...(replacement.previous ? { replacedRecord: replacement.previous } : {}) };
|
|
483
504
|
} catch (error) {
|
|
484
505
|
ctx.ui.notify(`Completed review is not available to publish-only: ${String(error)}`, "warning");
|
|
485
506
|
return completionError(
|
|
@@ -490,6 +511,12 @@ export default function registerReviewTable(
|
|
|
490
511
|
};
|
|
491
512
|
let outputRepairAttempted = false;
|
|
492
513
|
let outputRepairCancelled = false;
|
|
514
|
+
let outputRepairGeneration = 0;
|
|
515
|
+
const clearOutputRepair = () => {
|
|
516
|
+
outputRepairGeneration++;
|
|
517
|
+
outputRepairAttempted = false;
|
|
518
|
+
outputRepairCancelled = false;
|
|
519
|
+
};
|
|
493
520
|
const telemetryTracker = new ReviewTelemetryTracker();
|
|
494
521
|
const persistTelemetry = (completion: ReviewPerformanceTelemetry["completion"]) => {
|
|
495
522
|
const telemetry = telemetryTracker.finish(completion);
|
|
@@ -563,8 +590,7 @@ export default function registerReviewTable(
|
|
|
563
590
|
loopCoordinator.clear();
|
|
564
591
|
selfReviewCoordinator.clear();
|
|
565
592
|
pendingCompletion = undefined;
|
|
566
|
-
|
|
567
|
-
outputRepairCancelled = false;
|
|
593
|
+
clearOutputRepair();
|
|
568
594
|
telemetryTracker.clear();
|
|
569
595
|
};
|
|
570
596
|
|
|
@@ -586,8 +612,7 @@ export default function registerReviewTable(
|
|
|
586
612
|
selfReviewCoordinator.clear();
|
|
587
613
|
if (!outputRepairCancelled) return;
|
|
588
614
|
loopCoordinator.clear();
|
|
589
|
-
|
|
590
|
-
outputRepairCancelled = false;
|
|
615
|
+
clearOutputRepair();
|
|
591
616
|
persistTelemetry("cleared");
|
|
592
617
|
});
|
|
593
618
|
|
|
@@ -595,8 +620,7 @@ export default function registerReviewTable(
|
|
|
595
620
|
loopCoordinator.clear();
|
|
596
621
|
selfReviewCoordinator.clear();
|
|
597
622
|
pendingCompletion = undefined;
|
|
598
|
-
|
|
599
|
-
outputRepairCancelled = false;
|
|
623
|
+
clearOutputRepair();
|
|
600
624
|
restoreCompletedReviews(ctx);
|
|
601
625
|
telemetryTracker.clear();
|
|
602
626
|
const session = sessionIdentity(ctx);
|
|
@@ -615,11 +639,14 @@ export default function registerReviewTable(
|
|
|
615
639
|
selfReviewCoordinator.clear();
|
|
616
640
|
if (outputRepairAttempted) {
|
|
617
641
|
outputRepairCancelled = true;
|
|
642
|
+
loopCoordinator.clear();
|
|
643
|
+
clearOutputRepair();
|
|
644
|
+
persistTelemetry("cleared");
|
|
618
645
|
ctx.abort();
|
|
619
|
-
ctx.ui.notify("PR review output correction was cancelled; overlapping input was not queued, so retry it
|
|
646
|
+
ctx.ui.notify("PR review output correction was cancelled; overlapping input was not queued, so retry it", "warning");
|
|
620
647
|
return { action: "handled" as const };
|
|
621
648
|
}
|
|
622
|
-
|
|
649
|
+
clearOutputRepair();
|
|
623
650
|
const source = event.source as ReviewLoopInputSource;
|
|
624
651
|
const directPublish = parseDirectPublishRequest(event.text);
|
|
625
652
|
if (
|
|
@@ -748,8 +775,7 @@ export default function registerReviewTable(
|
|
|
748
775
|
ctx.ui.notify("PR review was not posted: automatic output correction attempted to call tools", "error");
|
|
749
776
|
ctx.abort();
|
|
750
777
|
loopCoordinator.clear();
|
|
751
|
-
|
|
752
|
-
outputRepairCancelled = false;
|
|
778
|
+
clearOutputRepair();
|
|
753
779
|
persistTelemetry("cleared");
|
|
754
780
|
return;
|
|
755
781
|
}
|
|
@@ -766,8 +792,7 @@ export default function registerReviewTable(
|
|
|
766
792
|
}
|
|
767
793
|
if (completion === "clear_invocation") {
|
|
768
794
|
loopCoordinator.clear();
|
|
769
|
-
|
|
770
|
-
outputRepairCancelled = false;
|
|
795
|
+
clearOutputRepair();
|
|
771
796
|
persistTelemetry("cleared");
|
|
772
797
|
return;
|
|
773
798
|
}
|
|
@@ -787,22 +812,63 @@ export default function registerReviewTable(
|
|
|
787
812
|
if (active && !publishable?.review && !outputRepairAttempted) {
|
|
788
813
|
const error = publishable?.error ?? "final review JSON is invalid";
|
|
789
814
|
if (loopCoordinator.suspendToolsForRepair()) {
|
|
815
|
+
const lease = loopCoordinator.repairLease(ctx);
|
|
816
|
+
if (!lease) {
|
|
817
|
+
ctx.ui.notify("Automatic PR review output correction was skipped because its review lease was lost", "warning");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
790
820
|
outputRepairAttempted = true;
|
|
791
821
|
outputRepairCancelled = false;
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
822
|
+
const repairGeneration = ++outputRepairGeneration;
|
|
823
|
+
const isActiveRepair = () => !outputRepairCancelled && repairGeneration === outputRepairGeneration &&
|
|
824
|
+
loopCoordinator.isRepairLeaseActive(lease, ctx);
|
|
825
|
+
ctx.ui.notify(`PR review output is invalid (${error}); asking the light repair subagent once`, "warning");
|
|
826
|
+
void repairOutput(text, REVIEW_OUTPUT_CONTRACT, ctx, lease.signal).then(async (repairedText) => {
|
|
827
|
+
if (!isActiveRepair()) return;
|
|
828
|
+
const repaired = repairedText ? parsePublishableReview(repairedText) : undefined;
|
|
829
|
+
if (!repaired?.review) {
|
|
830
|
+
if (isActiveRepair()) {
|
|
831
|
+
loopCoordinator.consume();
|
|
832
|
+
clearOutputRepair();
|
|
833
|
+
persistTelemetry("terminal_response");
|
|
834
|
+
ctx.ui.notify("PR review was not posted: light output correction did not produce valid final JSON", "error");
|
|
835
|
+
}
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const invocation = loopCoordinator.peek();
|
|
839
|
+
if (!invocation || !isActiveRepair()) return;
|
|
840
|
+
const completion = await resolveCompletion(repaired, invocation, ctx);
|
|
841
|
+
if (!isActiveRepair()) {
|
|
842
|
+
if (completion && "record" in completion) {
|
|
843
|
+
completedReviews.restoreReplacement(completion.record, completion.replacedRecord);
|
|
844
|
+
}
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
if (completion && "record" in completion) {
|
|
848
|
+
if (completion.session) {
|
|
849
|
+
try {
|
|
850
|
+
pi.appendEntry(COMPLETED_REVIEW_ENTRY_TYPE, completedReviews.persist(completion.record, completion.session));
|
|
851
|
+
} catch (persistError) {
|
|
852
|
+
ctx.ui.notify(`Completed review cache will not survive an extension reload: ${String(persistError)}`, "warning");
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (!isActiveRepair()) return;
|
|
856
|
+
await publishCompletedReview(completion.record, { kind: "frozen-invocation" }, ctx);
|
|
857
|
+
} else if (completion && "error" in completion) {
|
|
858
|
+
ctx.ui.notify(`PR review was not posted: ${completion.error}`, "error");
|
|
859
|
+
}
|
|
860
|
+
if (isActiveRepair()) {
|
|
861
|
+
loopCoordinator.consume();
|
|
862
|
+
clearOutputRepair();
|
|
863
|
+
persistTelemetry("terminal_response");
|
|
864
|
+
}
|
|
865
|
+
}).catch(() => {
|
|
866
|
+
if (!isActiveRepair()) return;
|
|
867
|
+
loopCoordinator.consume();
|
|
868
|
+
clearOutputRepair();
|
|
869
|
+
persistTelemetry("terminal_response");
|
|
870
|
+
ctx.ui.notify("PR review was not posted: light output correction failed", "error");
|
|
871
|
+
});
|
|
806
872
|
return;
|
|
807
873
|
}
|
|
808
874
|
ctx.ui.notify("Automatic PR review output correction was skipped because tools could not be disabled", "warning");
|
|
@@ -812,8 +878,7 @@ export default function registerReviewTable(
|
|
|
812
878
|
// Persist timing before publication so network/write latency is never coupled to review wall time.
|
|
813
879
|
const invocation = active ? loopCoordinator.consume() : undefined;
|
|
814
880
|
if (invocation) {
|
|
815
|
-
|
|
816
|
-
outputRepairCancelled = false;
|
|
881
|
+
clearOutputRepair();
|
|
817
882
|
persistTelemetry("terminal_response");
|
|
818
883
|
}
|
|
819
884
|
const review = publishable?.review
|
package/lib/pr-review-loop.ts
CHANGED
|
@@ -233,6 +233,24 @@ export class ReviewLoopCoordinator {
|
|
|
233
233
|
this.setToolsEnabled(false);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/** Return the current loop's abortable lease while its tools are suspended for output repair. */
|
|
237
|
+
repairLease(ctx: Pick<ExtensionContext, "cwd" | "sessionManager">): ReviewLoopLease | undefined {
|
|
238
|
+
const phase = this.invocationGate.phase();
|
|
239
|
+
if (!this.binding || this.suspendedTools === undefined || (phase !== "reviewing" && phase !== "confirmed")) return undefined;
|
|
240
|
+
if (!sameBinding(this.binding, ctx) || this.binding.controller.signal.aborted) {
|
|
241
|
+
this.clear();
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
return Object.freeze({ generation: this.binding.generation, signal: this.binding.controller.signal });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Check a repair lease without re-enabling the review tools. */
|
|
248
|
+
isRepairLeaseActive(lease: ReviewLoopLease, ctx: Pick<ExtensionContext, "cwd" | "sessionManager">): boolean {
|
|
249
|
+
return !!this.binding && this.suspendedTools !== undefined &&
|
|
250
|
+
this.binding.generation === lease.generation && !lease.signal.aborted &&
|
|
251
|
+
(this.phase() === "reviewing" || this.phase() === "confirmed") && sameBinding(this.binding, ctx);
|
|
252
|
+
}
|
|
253
|
+
|
|
236
254
|
/** Hide every tool for a format-only repair turn while retaining invocation authority. */
|
|
237
255
|
suspendToolsForRepair(): boolean {
|
|
238
256
|
const phase = this.invocationGate.phase();
|
package/lib/pr-review-publish.ts
CHANGED
|
@@ -603,17 +603,26 @@ export class CompletedReviewCache {
|
|
|
603
603
|
private readonly reviews = new Map<string, CompletedReviewRecord>();
|
|
604
604
|
|
|
605
605
|
remember(review: ReviewLike, invocation: ReviewInvocation, repository: RepositoryBinding): CompletedReviewRecord {
|
|
606
|
+
return this.replace(review, invocation, repository).record;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Replace one PR record while retaining its predecessor for cancellation rollback. */
|
|
610
|
+
replace(review: ReviewLike, invocation: ReviewInvocation, repository: RepositoryBinding): {
|
|
611
|
+
record: CompletedReviewRecord;
|
|
612
|
+
previous?: CompletedReviewRecord;
|
|
613
|
+
} {
|
|
606
614
|
const record = {
|
|
607
615
|
review,
|
|
608
616
|
invocation: { ...invocation, autoPost: { ...invocation.autoPost } },
|
|
609
617
|
repository: { ...repository },
|
|
610
618
|
};
|
|
611
619
|
const key = completedReviewKey(repository, invocation.prNumber);
|
|
620
|
+
const previous = this.reviews.get(key);
|
|
612
621
|
// Refresh insertion order so unnumbered direct publish requests bind to
|
|
613
622
|
// the most recently completed review in this repository.
|
|
614
623
|
this.reviews.delete(key);
|
|
615
624
|
this.reviews.set(key, record);
|
|
616
|
-
return record;
|
|
625
|
+
return previous ? { record, previous } : { record };
|
|
617
626
|
}
|
|
618
627
|
|
|
619
628
|
persist(
|
|
@@ -675,6 +684,20 @@ export class CompletedReviewCache {
|
|
|
675
684
|
return true;
|
|
676
685
|
}
|
|
677
686
|
|
|
687
|
+
/** Remove this exact record without deleting a newer completion for the same PR. */
|
|
688
|
+
forget(record: CompletedReviewRecord): void {
|
|
689
|
+
const key = completedReviewKey(record.repository, record.invocation.prNumber);
|
|
690
|
+
if (this.reviews.get(key) === record) this.reviews.delete(key);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Undo a replacement only when this exact record is still current. */
|
|
694
|
+
restoreReplacement(record: CompletedReviewRecord, previous: CompletedReviewRecord | undefined): void {
|
|
695
|
+
const key = completedReviewKey(record.repository, record.invocation.prNumber);
|
|
696
|
+
if (this.reviews.get(key) !== record) return;
|
|
697
|
+
this.reviews.delete(key);
|
|
698
|
+
if (previous) this.reviews.set(key, previous);
|
|
699
|
+
}
|
|
700
|
+
|
|
678
701
|
get(prNumber: number, repository: RepositoryBinding): CompletedReviewRecord | undefined {
|
|
679
702
|
return this.reviews.get(completedReviewKey(repository, prNumber));
|
|
680
703
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pr-review",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.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 safe COMMENT-only publishing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|