@mrclrchtr/supi-review 2.4.1 → 2.6.0

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/src/types.ts CHANGED
@@ -86,6 +86,9 @@ export interface ReviewSnapshot {
86
86
  stats: DiffStats;
87
87
  }
88
88
 
89
+ /** Snapshot metadata safe to retain without duplicating bulk diff text. */
90
+ export type ReviewSnapshotSummary = Omit<ReviewSnapshot, "diffText">;
91
+
89
92
  /** Model picked explicitly for the current review run. */
90
93
  export type ReviewModelSelection = import("@mrclrchtr/supi-core/model-selection").ModelSelection;
91
94
 
@@ -107,6 +110,107 @@ export interface SynthesizedReviewBrief {
107
110
  note?: string;
108
111
  }
109
112
 
113
+ /** Brief field evaluated by the main agent before reviewer sessions run. */
114
+ export type ReviewBriefField =
115
+ | "summary"
116
+ | "intendedOutcome"
117
+ | "constraints"
118
+ | "focusAreas"
119
+ | "riskyFiles"
120
+ | "unresolvedQuestions"
121
+ | "reviewInstructionBlockIds";
122
+
123
+ /** Class of defect identified in a generated review brief. */
124
+ export type BriefCritiqueFindingKind =
125
+ | "omission"
126
+ | "unsupported-inference"
127
+ | "misprioritized"
128
+ | "unclear";
129
+
130
+ /** One evidence-backed main-agent criticism of a generated review brief. */
131
+ export interface BriefCritiqueFinding {
132
+ kind: BriefCritiqueFindingKind;
133
+ field: ReviewBriefField;
134
+ explanation: string;
135
+ evidence: string;
136
+ proposedChange: string;
137
+ }
138
+
139
+ /** Structured quality gate completed by the main agent before review execution. */
140
+ export interface BriefCritique {
141
+ verdict: "accept" | "revise";
142
+ summary: string;
143
+ findings: BriefCritiqueFinding[];
144
+ }
145
+
146
+ /** One independent reviewer child-session assignment. */
147
+ export interface ReviewerAssignment {
148
+ id: string;
149
+ focus: string;
150
+ }
151
+
152
+ /** Generated/effective brief pair retained for synthesis-prompt evaluation. */
153
+ export interface BriefEvaluation {
154
+ planId: string;
155
+ briefPromptVersion: string;
156
+ generatedBrief: SynthesizedReviewBrief;
157
+ critique: BriefCritique;
158
+ effectiveBrief: SynthesizedReviewBrief;
159
+ synthesizerModelId: string;
160
+ snapshotFingerprint: string;
161
+ }
162
+
163
+ /** Normalized reviewer result without repeated snapshot or brief payloads. */
164
+ export type AgentReviewerResult =
165
+ | {
166
+ kind: "success";
167
+ output: NormalizedReviewOutput;
168
+ modelId: string;
169
+ }
170
+ | {
171
+ kind: "failed";
172
+ reason: string;
173
+ modelId: string;
174
+ debug?: ReviewFailureDebugInfo;
175
+ }
176
+ | {
177
+ kind: "canceled";
178
+ modelId: string;
179
+ debug?: ReviewFailureDebugInfo;
180
+ }
181
+ | {
182
+ kind: "timeout";
183
+ timeoutMs: number;
184
+ partialOutput?: string;
185
+ modelId: string;
186
+ debug?: ReviewFailureDebugInfo;
187
+ };
188
+
189
+ /** One normalized result from a focused reviewer child session. */
190
+ export interface ReviewerAssignmentResult {
191
+ assignment: ReviewerAssignment;
192
+ result: AgentReviewerResult;
193
+ }
194
+
195
+ /** Structured details retained on a completed agent-driven review batch. */
196
+ export interface AgentReviewBatchDetails {
197
+ kind: "review-batch";
198
+ evaluation: BriefEvaluation;
199
+ snapshot: ReviewSnapshotSummary;
200
+ results: ReviewerAssignmentResult[];
201
+ }
202
+
203
+ /** Structured details returned when an agent-driven review plan is prepared. */
204
+ export interface PreparedAgentReviewDetails {
205
+ kind: "review-prepared";
206
+ planId: string;
207
+ briefPromptVersion: string;
208
+ generatedBrief: SynthesizedReviewBrief;
209
+ snapshot: ReviewSnapshotSummary;
210
+ snapshotFingerprint: string;
211
+ modelId: string;
212
+ }
213
+
110
214
  /** Final prompt packet passed to the reviewer child session. */
111
215
  export interface ReviewPacket {
112
216
  prompt: string;
@@ -0,0 +1,138 @@
1
+ import type {
2
+ AgentReviewBatchDetails,
3
+ AgentReviewerResult,
4
+ BriefCritique,
5
+ PreparedAgentReviewDetails,
6
+ ReviewResult,
7
+ ReviewSnapshotSummary,
8
+ SynthesizedReviewBrief,
9
+ } from "../types.ts";
10
+ import { formatReviewContent } from "./format-content.ts";
11
+
12
+ /** Format a generated brief and the required main-agent quality gate. */
13
+ export function formatPreparedAgentReview(details: PreparedAgentReviewDetails): string {
14
+ return [
15
+ "# Review Brief Prepared",
16
+ "",
17
+ `Plan ID: ${details.planId}`,
18
+ `Brief prompt version: ${details.briefPromptVersion}`,
19
+ `Model: ${details.modelId}`,
20
+ `Snapshot: ${details.snapshot.title}`,
21
+ `Snapshot fingerprint: ${details.snapshotFingerprint}`,
22
+ `Files changed: ${details.snapshot.changedFiles.length}`,
23
+ `Diff stats: +${details.snapshot.stats.additions} / -${details.snapshot.stats.deletions}`,
24
+ "",
25
+ "## Generated brief",
26
+ "",
27
+ ...formatBrief(details.generatedBrief),
28
+ "",
29
+ "## Required next step",
30
+ "",
31
+ "Critically compare this brief with the user request, session evidence, and snapshot.",
32
+ "Then call supi_review_run with this planId, an evidence-backed critique, and one to four reviewer assignments.",
33
+ 'When the critique verdict is "revise", provide the full corrected revisedBrief.',
34
+ "Do not mutate the review target before running the prepared plan, and call supi_review_run without sibling mutation tools.",
35
+ "",
36
+ "## Changed files",
37
+ "",
38
+ ...details.snapshot.changedFiles.map((file) => `- ${file}`),
39
+ ].join("\n");
40
+ }
41
+
42
+ /** Format a completed review batch for the parent agent. */
43
+ export function formatAgentReviewBatch(details: AgentReviewBatchDetails): string {
44
+ const lines = [
45
+ "# Review Batch Complete",
46
+ "",
47
+ `Plan ID: ${details.evaluation.planId}`,
48
+ `Snapshot: ${details.snapshot.title}`,
49
+ `Model: ${details.evaluation.synthesizerModelId}`,
50
+ `Brief critique: ${details.evaluation.critique.verdict.toUpperCase()} — ${details.evaluation.critique.summary}`,
51
+ `Brief critique findings: ${details.evaluation.critique.findings.length}`,
52
+ ];
53
+
54
+ for (const { assignment, result } of details.results) {
55
+ lines.push(
56
+ "",
57
+ "---",
58
+ "",
59
+ `## Reviewer: ${assignment.id}`,
60
+ "",
61
+ `Focus: ${assignment.focus}`,
62
+ "",
63
+ formatReviewContent(hydrateResult(result, details.snapshot)),
64
+ );
65
+ }
66
+
67
+ lines.push(
68
+ "",
69
+ "---",
70
+ "",
71
+ "## Retained main-agent brief critique",
72
+ "",
73
+ ...formatCritique(details.evaluation.critique),
74
+ );
75
+ if (details.evaluation.critique.verdict === "revise") {
76
+ lines.push(
77
+ "",
78
+ "## Effective revised brief",
79
+ "",
80
+ ...formatBrief(details.evaluation.effectiveBrief),
81
+ );
82
+ }
83
+
84
+ return lines.join("\n");
85
+ }
86
+
87
+ /** Format one structured main-agent brief critique. */
88
+ export function formatCritique(critique: BriefCritique): string[] {
89
+ const lines = [`Verdict: ${critique.verdict.toUpperCase()}`, `Summary: ${critique.summary}`];
90
+ if (critique.findings.length === 0) {
91
+ lines.push("Findings: none");
92
+ return lines;
93
+ }
94
+
95
+ lines.push("Findings:");
96
+ for (const [index, finding] of critique.findings.entries()) {
97
+ lines.push(
98
+ `${index + 1}. [${finding.kind}] ${finding.field}: ${finding.explanation}`,
99
+ ` Evidence: ${finding.evidence}`,
100
+ ` Proposed change: ${finding.proposedChange}`,
101
+ );
102
+ }
103
+ return lines;
104
+ }
105
+
106
+ /** Format all structured fields of a synthesized review brief. */
107
+ export function formatBrief(brief: SynthesizedReviewBrief): string[] {
108
+ const lines = [
109
+ `Summary: ${brief.summary}`,
110
+ `Intended outcome: ${brief.intendedOutcome}`,
111
+ ...formatList("Constraints", brief.constraints),
112
+ ...formatList("Focus areas", brief.focusAreas),
113
+ ...formatList("Risky files", brief.riskyFiles),
114
+ ...formatList("Unresolved questions", brief.unresolvedQuestions),
115
+ ...formatList("Review instruction blocks", brief.reviewInstructionBlockIds),
116
+ ];
117
+ if (brief.note) lines.push(`Note: ${brief.note}`);
118
+ return lines;
119
+ }
120
+
121
+ function formatList(label: string, values: readonly string[]): string[] {
122
+ if (values.length === 0) return [`${label}: none`];
123
+ return [`${label}:`, ...values.map((value) => `- ${value}`)];
124
+ }
125
+
126
+ function hydrateResult(result: AgentReviewerResult, summary: ReviewSnapshotSummary): ReviewResult {
127
+ const snapshot = { ...summary, diffText: "" };
128
+ switch (result.kind) {
129
+ case "success":
130
+ return { ...result, snapshot };
131
+ case "failed":
132
+ return { ...result, snapshot };
133
+ case "canceled":
134
+ return { ...result, snapshot };
135
+ case "timeout":
136
+ return { ...result, snapshot };
137
+ }
138
+ }
@@ -0,0 +1,203 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Container, Spacer, Text } from "@earendil-works/pi-tui";
3
+ import type { PrepareAgentReviewInput, RunAgentReviewInput } from "../tool/agent-review-schemas.ts";
4
+ import type {
5
+ AgentReviewBatchDetails,
6
+ PreparedAgentReviewDetails,
7
+ ReviewProgress,
8
+ } from "../types.ts";
9
+ import { formatBrief, formatCritique } from "./review-tool-format.ts";
10
+
11
+ type ReviewTheme = Parameters<Parameters<ExtensionAPI["registerMessageRenderer"]>[1]>[2];
12
+
13
+ interface ToolResultLike {
14
+ content: Array<{ type: string; text?: string }>;
15
+ details?: unknown;
16
+ }
17
+
18
+ /** Partial tool-result details used while synthesis or reviewer sessions are active. */
19
+ export interface AgentReviewProgressDetails {
20
+ kind: "review-progress";
21
+ phase: "prepare" | "review";
22
+ completed: number;
23
+ total: number;
24
+ reviewers?: Record<string, ReviewProgress>;
25
+ }
26
+
27
+ export function renderPrepareReviewCall(args: PrepareAgentReviewInput, theme: ReviewTheme): Text {
28
+ const target = args.target?.kind ?? "working-tree";
29
+ const ref = args.target?.base ?? args.target?.sha;
30
+ const suffix = ref ? ` (${ref})` : "";
31
+ return new Text(
32
+ `${theme.fg("toolTitle", theme.bold("supi_review_prepare "))}${theme.fg("accent", target)}${theme.fg("dim", suffix)}`,
33
+ 0,
34
+ 0,
35
+ );
36
+ }
37
+
38
+ export function renderPrepareReviewResult(
39
+ result: ToolResultLike,
40
+ expanded: boolean,
41
+ theme: ReviewTheme,
42
+ ): Container | Text {
43
+ const details = result.details as
44
+ | PreparedAgentReviewDetails
45
+ | AgentReviewProgressDetails
46
+ | undefined;
47
+ if (details?.kind === "review-progress") {
48
+ return new Text(theme.fg("warning", "Synthesizing review brief…"), 0, 0);
49
+ }
50
+ if (details?.kind !== "review-prepared") return fallbackResult(result, theme);
51
+
52
+ const container = new Container();
53
+ container.addChild(
54
+ new Text(
55
+ `${theme.fg("success", "✓")} ${theme.fg("toolTitle", theme.bold("Review brief prepared"))}`,
56
+ 0,
57
+ 0,
58
+ ),
59
+ );
60
+ container.addChild(new Text(theme.fg("dim", `Plan: ${details.planId}`), 0, 0));
61
+ container.addChild(new Text(theme.fg("dim", `Snapshot: ${details.snapshot.title}`), 0, 0));
62
+ container.addChild(
63
+ new Text(theme.fg("muted", `Summary: ${details.generatedBrief.summary}`), 0, 0),
64
+ );
65
+ container.addChild(
66
+ new Text(theme.fg("warning", "Main-agent critique required before run"), 0, 0),
67
+ );
68
+
69
+ if (expanded) {
70
+ container.addChild(new Spacer(1));
71
+ for (const line of formatBrief(details.generatedBrief)) {
72
+ container.addChild(new Text(theme.fg("dim", line), 0, 0));
73
+ }
74
+ }
75
+ return container;
76
+ }
77
+
78
+ export function renderRunReviewCall(
79
+ args: RunAgentReviewInput,
80
+ expanded: boolean,
81
+ theme: ReviewTheme,
82
+ ): Container | Text {
83
+ const verdictColor = args.critique.verdict === "revise" ? "warning" : "success";
84
+ const header = `${theme.fg("toolTitle", theme.bold("supi_review_run "))}${theme.fg(verdictColor, args.critique.verdict)}${theme.fg("dim", ` · ${args.critique.findings.length} critique finding(s) · ${args.reviewers.length} reviewer(s)`)}`;
85
+ if (!expanded) return new Text(header, 0, 0);
86
+
87
+ const container = new Container();
88
+ container.addChild(new Text(header, 0, 0));
89
+ for (const line of formatCritique(args.critique)) {
90
+ container.addChild(new Text(theme.fg("dim", line), 0, 0));
91
+ }
92
+ if (args.revisedBrief) {
93
+ container.addChild(new Spacer(1));
94
+ container.addChild(new Text(theme.fg("accent", "Proposed revised brief"), 0, 0));
95
+ for (const line of formatBrief(args.revisedBrief)) {
96
+ container.addChild(new Text(theme.fg("dim", line), 0, 0));
97
+ }
98
+ }
99
+ container.addChild(new Spacer(1));
100
+ for (const reviewer of args.reviewers) {
101
+ container.addChild(new Text(theme.fg("muted", `${reviewer.id}: ${reviewer.focus}`), 0, 0));
102
+ }
103
+ return container;
104
+ }
105
+
106
+ export function renderRunReviewResult(
107
+ result: ToolResultLike,
108
+ expanded: boolean,
109
+ theme: ReviewTheme,
110
+ ): Container | Text {
111
+ const details = result.details as
112
+ | AgentReviewBatchDetails
113
+ | AgentReviewProgressDetails
114
+ | undefined;
115
+ if (details?.kind === "review-progress") {
116
+ return renderProgress(details, theme);
117
+ }
118
+ if (details?.kind !== "review-batch") return fallbackResult(result, theme);
119
+
120
+ const container = new Container();
121
+ const successCount = details.results.filter((entry) => entry.result.kind === "success").length;
122
+ const verdictColor = details.evaluation.critique.verdict === "revise" ? "warning" : "success";
123
+ container.addChild(
124
+ new Text(
125
+ `${theme.fg("success", "✓")} ${theme.fg("toolTitle", theme.bold("Review batch complete"))}${theme.fg("dim", ` · ${successCount}/${details.results.length} succeeded`)}`,
126
+ 0,
127
+ 0,
128
+ ),
129
+ );
130
+ container.addChild(
131
+ new Text(
132
+ theme.fg(
133
+ verdictColor,
134
+ `Brief ${details.evaluation.critique.verdict}: ${details.evaluation.critique.summary}`,
135
+ ),
136
+ 0,
137
+ 0,
138
+ ),
139
+ );
140
+
141
+ for (const entry of details.results) {
142
+ const status = summarizeResult(entry.result);
143
+ container.addChild(
144
+ new Text(
145
+ `${theme.fg(status.color, status.icon)} ${entry.assignment.id}: ${status.text}`,
146
+ 0,
147
+ 0,
148
+ ),
149
+ );
150
+ }
151
+
152
+ if (expanded) {
153
+ container.addChild(new Spacer(1));
154
+ for (const line of formatCritique(details.evaluation.critique)) {
155
+ container.addChild(new Text(theme.fg("dim", line), 0, 0));
156
+ }
157
+ if (details.evaluation.critique.verdict === "revise") {
158
+ container.addChild(new Spacer(1));
159
+ container.addChild(new Text(theme.fg("accent", "Effective revised brief"), 0, 0));
160
+ for (const line of formatBrief(details.evaluation.effectiveBrief)) {
161
+ container.addChild(new Text(theme.fg("dim", line), 0, 0));
162
+ }
163
+ }
164
+ }
165
+ return container;
166
+ }
167
+
168
+ function renderProgress(details: AgentReviewProgressDetails, theme: ReviewTheme): Text {
169
+ if (details.phase === "prepare") {
170
+ return new Text(theme.fg("warning", "Synthesizing review brief…"), 0, 0);
171
+ }
172
+ return new Text(
173
+ theme.fg("warning", `Running reviewers… ${details.completed}/${details.total} completed`),
174
+ 0,
175
+ 0,
176
+ );
177
+ }
178
+
179
+ function summarizeResult(result: AgentReviewBatchDetails["results"][number]["result"]): {
180
+ icon: string;
181
+ color: "success" | "warning" | "error";
182
+ text: string;
183
+ } {
184
+ switch (result.kind) {
185
+ case "success":
186
+ return {
187
+ icon: "✓",
188
+ color: result.output.items.length > 0 ? "warning" : "success",
189
+ text: `${result.output.items.length} item(s), ${result.output.overall_correctness}`,
190
+ };
191
+ case "failed":
192
+ return { icon: "✗", color: "error", text: result.reason };
193
+ case "canceled":
194
+ return { icon: "○", color: "warning", text: "canceled" };
195
+ case "timeout":
196
+ return { icon: "◷", color: "warning", text: "timed out" };
197
+ }
198
+ }
199
+
200
+ function fallbackResult(result: ToolResultLike, theme: ReviewTheme): Text {
201
+ const content = result.content.find((part) => part.type === "text")?.text ?? "No review output";
202
+ return new Text(theme.fg("dim", content), 0, 0);
203
+ }