@mrclrchtr/supi-review 2.8.0 → 3.1.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/README.md +71 -180
- package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +40 -0
- package/package.json +3 -3
- package/src/config.ts +30 -21
- package/src/git-command.ts +78 -0
- package/src/git.ts +311 -507
- package/src/history/collect.ts +43 -120
- package/src/model.ts +3 -1
- package/src/review-path.ts +85 -0
- package/src/review-result.ts +43 -92
- package/src/review.ts +187 -286
- package/src/session/review-plan-store.ts +20 -51
- package/src/target/packet.ts +46 -369
- package/src/tool/agent-review-schemas.ts +108 -104
- package/src/tool/agent-review-tools.ts +283 -307
- package/src/tool/child-failure-diagnostics.ts +59 -1
- package/src/tool/child-lifecycle-trace.ts +32 -3
- package/src/tool/child-resource-loader.ts +37 -0
- package/src/tool/child-session-runner.ts +83 -0
- package/src/tool/output-page.ts +47 -0
- package/src/tool/planner-runner.ts +69 -0
- package/src/tool/review-runner.ts +42 -257
- package/src/tool/review-system-prompt.ts +12 -110
- package/src/tool/review-tools.ts +119 -0
- package/src/tool/review-workflow.ts +325 -0
- package/src/tool/runner-helpers.ts +3 -8
- package/src/tool/schemas.ts +75 -62
- package/src/tui/common.ts +237 -0
- package/src/tui/prepare.ts +132 -0
- package/src/tui/run.ts +160 -0
- package/src/types.ts +157 -275
- package/src/history/synthesize.ts +0 -121
- package/src/tool/agent-review-workflow.ts +0 -398
- package/src/tool/brief-runner.ts +0 -180
- package/src/tool/guidance.ts +0 -21
- package/src/tool/review-handlers.ts +0 -314
- package/src/tool/snapshot-tools.ts +0 -117
- package/src/ui/flow.ts +0 -189
- package/src/ui/format-content.ts +0 -143
- package/src/ui/renderer.ts +0 -274
- package/src/ui/review-plan-inspector.ts +0 -391
- package/src/ui/review-tool-format.ts +0 -138
- package/src/ui/review-tool-renderer.ts +0 -234
|
@@ -1,342 +1,318 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
buildSessionContext,
|
|
6
|
-
DEFAULT_MAX_BYTES,
|
|
7
|
-
DEFAULT_MAX_LINES,
|
|
8
|
-
type ExtensionAPI,
|
|
9
|
-
formatSize,
|
|
10
|
-
truncateHead,
|
|
11
|
-
} from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
|
|
1
|
+
import { buildSessionContext, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { StatusSpinner } from "@mrclrchtr/supi-core/status-spinner";
|
|
13
3
|
import { loadReviewConfig } from "../config.ts";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { ReviewPlanStore } from "../session/review-plan-store.ts";
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
ReviewProgress,
|
|
22
|
-
ReviewTargetSpec,
|
|
23
|
-
} from "../types.ts";
|
|
24
|
-
import { formatBriefSynthesisFailureCopy } from "../ui/format-content.ts";
|
|
25
|
-
import { formatAgentReviewBatch, formatPreparedAgentReview } from "../ui/review-tool-format.ts";
|
|
26
|
-
import {
|
|
27
|
-
type AgentReviewProgressDetails,
|
|
28
|
-
renderPrepareReviewCall,
|
|
29
|
-
renderPrepareReviewResult,
|
|
30
|
-
renderRunReviewCall,
|
|
31
|
-
renderRunReviewResult,
|
|
32
|
-
} from "../ui/review-tool-renderer.ts";
|
|
4
|
+
import { summarizeReviewSnapshot } from "../git.ts";
|
|
5
|
+
import { collectPlannerContext } from "../history/collect.ts";
|
|
6
|
+
import { resolveAgentReviewModel } from "../model.ts";
|
|
7
|
+
import type { ReviewPlanStore } from "../session/review-plan-store.ts";
|
|
8
|
+
import { renderPrepareCall, renderPrepareResult } from "../tui/prepare.ts";
|
|
9
|
+
import { renderRunCall, renderRunResult } from "../tui/run.ts";
|
|
10
|
+
import type { ReviewBatchDetails, ReviewTargetSpec, ReviewTaskResult } from "../types.ts";
|
|
33
11
|
import {
|
|
34
|
-
type
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
12
|
+
type PrepareReviewToolInput,
|
|
13
|
+
parsePrepareReviewToolInput,
|
|
14
|
+
parseRunReviewToolInput,
|
|
15
|
+
prepareReviewSchema,
|
|
16
|
+
runReviewSchema,
|
|
38
17
|
} from "./agent-review-schemas.ts";
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
PREPARE_REVIEW_TOOL_NAME,
|
|
43
|
-
prepareReviewPromptGuidelines,
|
|
44
|
-
prepareReviewPromptSnippet,
|
|
45
|
-
prepareReviewToolDescription,
|
|
46
|
-
RUN_REVIEW_TOOL_NAME,
|
|
47
|
-
runReviewToolDescription,
|
|
48
|
-
} from "./guidance.ts";
|
|
49
|
-
|
|
50
|
-
const TOOL_OUTPUT_CONTRACT =
|
|
51
|
-
" Output is truncated at 2,000 lines or 50KB; full Markdown is saved to a temporary file when truncated.";
|
|
52
|
-
|
|
53
|
-
/** Register the two-stage agent-driven Session-Aware Review tools. */
|
|
54
|
-
export function registerAgentReviewTools(
|
|
55
|
-
pi: ExtensionAPI,
|
|
56
|
-
planStore = new ReviewPlanStore(),
|
|
57
|
-
): void {
|
|
58
|
-
pi.registerTool({
|
|
59
|
-
name: PREPARE_REVIEW_TOOL_NAME,
|
|
60
|
-
label: "Prepare Review",
|
|
61
|
-
description: prepareReviewToolDescription + TOOL_OUTPUT_CONTRACT,
|
|
62
|
-
promptSnippet: prepareReviewPromptSnippet,
|
|
63
|
-
promptGuidelines: prepareReviewPromptGuidelines,
|
|
64
|
-
parameters: prepareAgentReviewSchema,
|
|
65
|
-
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
66
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
67
|
-
const input = params as PrepareAgentReviewInput;
|
|
68
|
-
const configuredModelId = loadReviewConfig(ctx.cwd).agentModel;
|
|
69
|
-
const model = resolveAgentReviewModel(ctx, configuredModelId);
|
|
70
|
-
if (!model) {
|
|
71
|
-
if (configuredModelId === CURRENT_SESSION_REVIEW_MODEL) {
|
|
72
|
-
throw new Error("No current session model is available for review preparation.");
|
|
73
|
-
}
|
|
74
|
-
throw new Error(
|
|
75
|
-
`Configured agent review model "${configuredModelId}" is not available in Pi's scoped model set. Choose another model in /supi-settings.`,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const target = parseTarget(input);
|
|
80
|
-
const sessionContext = buildSessionContext(
|
|
81
|
-
ctx.sessionManager.getEntries(),
|
|
82
|
-
ctx.sessionManager.getLeafId(),
|
|
83
|
-
);
|
|
84
|
-
const serializedContext = serializeSessionContext(sessionContext.messages);
|
|
85
|
-
const note = input.note?.trim() || undefined;
|
|
18
|
+
import { formatChildFailureDiagnostics } from "./child-failure-diagnostics.ts";
|
|
19
|
+
import { pageText } from "./output-page.ts";
|
|
20
|
+
import { prepareReview, runReview } from "./review-workflow.ts";
|
|
86
21
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
cwd: ctx.cwd,
|
|
91
|
-
target,
|
|
92
|
-
note,
|
|
93
|
-
serializedContext,
|
|
94
|
-
model,
|
|
95
|
-
signal,
|
|
96
|
-
planStore,
|
|
97
|
-
onProgress: (progress) => {
|
|
98
|
-
onUpdate?.({
|
|
99
|
-
content: [{ type: "text", text: "Synthesizing review brief…" }],
|
|
100
|
-
details: prepareProgressDetails(progress),
|
|
101
|
-
});
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
if (outcome.kind !== "prepared") {
|
|
105
|
-
throw new Error(formatPreparationFailure(outcome));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const details: PreparedAgentReviewDetails = {
|
|
109
|
-
kind: "review-prepared",
|
|
110
|
-
planId: outcome.plan.id,
|
|
111
|
-
briefPromptVersion: outcome.plan.briefPromptVersion,
|
|
112
|
-
generatedBrief: outcome.plan.generatedBrief,
|
|
113
|
-
snapshot: summarizeReviewSnapshot(outcome.plan.snapshot),
|
|
114
|
-
snapshotFingerprint: outcome.plan.snapshotFingerprint,
|
|
115
|
-
modelId: outcome.plan.model.canonicalId,
|
|
116
|
-
};
|
|
117
|
-
activateRunTool(pi);
|
|
118
|
-
return toolResult(formatPreparedAgentReview(details), details, PREPARE_REVIEW_TOOL_NAME);
|
|
119
|
-
} finally {
|
|
120
|
-
pi.events.emit("supi:working:end", { source: "supi-review" });
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
renderCall(args, theme) {
|
|
124
|
-
return renderPrepareReviewCall(args, theme);
|
|
125
|
-
},
|
|
126
|
-
renderResult(result, { expanded }, theme) {
|
|
127
|
-
return renderPrepareReviewResult(result, expanded, theme);
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
pi.registerTool({
|
|
132
|
-
name: RUN_REVIEW_TOOL_NAME,
|
|
133
|
-
label: "Run Review",
|
|
134
|
-
description: runReviewToolDescription + TOOL_OUTPUT_CONTRACT,
|
|
135
|
-
parameters: runAgentReviewSchema,
|
|
136
|
-
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
137
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
138
|
-
const input = params as RunAgentReviewInput;
|
|
139
|
-
const progress = createBatchProgress(input.reviewers.map((reviewer) => reviewer.id.trim()));
|
|
140
|
-
|
|
141
|
-
pi.events.emit("supi:working:start", { source: "supi-review" });
|
|
142
|
-
try {
|
|
143
|
-
const outcome = await runAgentReviewBatch({
|
|
144
|
-
cwd: ctx.cwd,
|
|
145
|
-
planId: input.planId,
|
|
146
|
-
critique: input.critique,
|
|
147
|
-
revisedBrief: input.revisedBrief,
|
|
148
|
-
reviewers: input.reviewers,
|
|
149
|
-
signal,
|
|
150
|
-
planStore,
|
|
151
|
-
onBriefEvaluation: (evaluation) => {
|
|
152
|
-
recordBriefCritique(ctx.cwd, evaluation, input.reviewers.length);
|
|
153
|
-
},
|
|
154
|
-
onReviewerProgress: (reviewerId, reviewerProgress) => {
|
|
155
|
-
progress.reviewers[reviewerId] = reviewerProgress;
|
|
156
|
-
onUpdate?.({
|
|
157
|
-
content: [
|
|
158
|
-
{
|
|
159
|
-
type: "text",
|
|
160
|
-
text: `Running reviewers… ${progress.completed}/${progress.total} completed`,
|
|
161
|
-
},
|
|
162
|
-
],
|
|
163
|
-
details: batchProgressDetails(progress),
|
|
164
|
-
});
|
|
165
|
-
},
|
|
166
|
-
onReviewerDone: (reviewerId) => {
|
|
167
|
-
progress.completed++;
|
|
168
|
-
progress.reviewers[reviewerId] = {
|
|
169
|
-
...(progress.reviewers[reviewerId] ?? { turns: 0, toolUses: 0 }),
|
|
170
|
-
currentFocus: undefined,
|
|
171
|
-
};
|
|
172
|
-
onUpdate?.({
|
|
173
|
-
content: [
|
|
174
|
-
{
|
|
175
|
-
type: "text",
|
|
176
|
-
text: `Running reviewers… ${progress.completed}/${progress.total} completed`,
|
|
177
|
-
},
|
|
178
|
-
],
|
|
179
|
-
details: batchProgressDetails(progress),
|
|
180
|
-
});
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
if (outcome.kind !== "completed") throw new Error(outcome.reason);
|
|
184
|
-
|
|
185
|
-
return toolResult(
|
|
186
|
-
formatAgentReviewBatch(outcome.details),
|
|
187
|
-
outcome.details,
|
|
188
|
-
RUN_REVIEW_TOOL_NAME,
|
|
189
|
-
);
|
|
190
|
-
} finally {
|
|
191
|
-
pi.events.emit("supi:working:end", { source: "supi-review" });
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
renderCall(args, theme, context) {
|
|
195
|
-
return renderRunReviewCall(args, context.expanded, theme);
|
|
196
|
-
},
|
|
197
|
-
renderResult(result, { expanded }, theme) {
|
|
198
|
-
return renderRunReviewResult(result, expanded, theme);
|
|
199
|
-
},
|
|
200
|
-
});
|
|
22
|
+
function target(input: PrepareReviewToolInput["target"]): ReviewTargetSpec {
|
|
23
|
+
return (input ?? { kind: "working-tree" }) as ReviewTargetSpec;
|
|
24
|
+
}
|
|
201
25
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
26
|
+
function formatPrepared(plan: {
|
|
27
|
+
id: string;
|
|
28
|
+
snapshot: { title: string; changedFiles: string[] };
|
|
29
|
+
plannerDraft?: { sharedContext?: string; tasks: Array<{ id: string; instructions: string }> };
|
|
30
|
+
}): string {
|
|
31
|
+
const lines = [
|
|
32
|
+
"# Review Plan Prepared",
|
|
33
|
+
"",
|
|
34
|
+
`Plan ID: ${plan.id}`,
|
|
35
|
+
`Target: ${plan.snapshot.title}`,
|
|
36
|
+
`Files changed: ${plan.snapshot.changedFiles.length}`,
|
|
37
|
+
];
|
|
38
|
+
if (plan.plannerDraft) {
|
|
39
|
+
lines.push("", "## Planner Draft");
|
|
40
|
+
if (plan.plannerDraft.sharedContext) lines.push("", plan.plannerDraft.sharedContext);
|
|
41
|
+
for (const task of plan.plannerDraft.tasks) {
|
|
42
|
+
lines.push("", `### ${task.id}`, task.instructions);
|
|
43
|
+
}
|
|
44
|
+
lines.push("", "Call supi_review_run with an explicit accept-draft or use-review decision.");
|
|
45
|
+
} else {
|
|
46
|
+
lines.push("", "Call supi_review_run with a use-review decision containing one to four tasks.");
|
|
47
|
+
}
|
|
48
|
+
return lines.join("\n");
|
|
209
49
|
}
|
|
210
50
|
|
|
211
|
-
function
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
51
|
+
function formatTaskResult(result: ReviewTaskResult): string[] {
|
|
52
|
+
const lines = [
|
|
53
|
+
"",
|
|
54
|
+
`## ${result.taskId}`,
|
|
55
|
+
`Model: ${result.modelId}`,
|
|
56
|
+
`Packet SHA-256: ${result.packetHash}`,
|
|
57
|
+
];
|
|
58
|
+
if (result.status === "failed") {
|
|
59
|
+
lines.push(`Status: failed (${result.failureCode})`);
|
|
60
|
+
if (result.diagnostics) {
|
|
61
|
+
lines.push("", ...formatChildFailureDiagnostics(result.diagnostics));
|
|
62
|
+
}
|
|
63
|
+
return lines;
|
|
64
|
+
}
|
|
65
|
+
if (result.status === "canceled") {
|
|
66
|
+
lines.push("Status: canceled");
|
|
67
|
+
if (result.diagnostics) {
|
|
68
|
+
lines.push("", ...formatChildFailureDiagnostics(result.diagnostics));
|
|
217
69
|
}
|
|
218
|
-
return
|
|
70
|
+
return lines;
|
|
219
71
|
}
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (base.startsWith("-")) {
|
|
225
|
-
throw new Error("target.base must name a local branch, not a Git option.");
|
|
72
|
+
if (result.status === "timeout") {
|
|
73
|
+
lines.push(`Status: timeout (${result.timeoutMs} ms)`);
|
|
74
|
+
if (result.diagnostics) {
|
|
75
|
+
lines.push("", ...formatChildFailureDiagnostics(result.diagnostics));
|
|
226
76
|
}
|
|
227
|
-
return
|
|
77
|
+
return lines;
|
|
228
78
|
}
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
79
|
+
lines.push(`Verdict: ${result.verdict.toUpperCase()}`, "", result.summary);
|
|
80
|
+
for (const finding of result.findings) {
|
|
81
|
+
lines.push(
|
|
82
|
+
"",
|
|
83
|
+
`- ${finding.title} [${finding.blocksAcceptance ? "blocking" : "non-blocking"}; impact ${finding.impact}; effort ${finding.effort}; confidence ${finding.confidence}]`,
|
|
84
|
+
...(finding.location
|
|
85
|
+
? [
|
|
86
|
+
` Location: ${finding.location.path}:${finding.location.startLine}-${finding.location.endLine}`,
|
|
87
|
+
]
|
|
88
|
+
: []),
|
|
89
|
+
` ${finding.description}`,
|
|
90
|
+
);
|
|
234
91
|
}
|
|
235
|
-
return
|
|
92
|
+
return lines;
|
|
236
93
|
}
|
|
237
94
|
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
95
|
+
export function formatReviewBatch(details: ReviewBatchDetails): string {
|
|
96
|
+
const lines = [
|
|
97
|
+
"# Review Complete",
|
|
98
|
+
"",
|
|
99
|
+
`Mode: ${details.mode}`,
|
|
100
|
+
`Provenance: ${details.provenance}`,
|
|
101
|
+
`Target: ${details.snapshot.title}`,
|
|
102
|
+
];
|
|
103
|
+
if (details.planning) {
|
|
104
|
+
lines.push(
|
|
105
|
+
`Planner: ${details.planning.modelId} (protocol ${details.planning.promptVersion})`,
|
|
106
|
+
`Planner decision: ${details.planning.decision}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
for (const result of details.results) lines.push(...formatTaskResult(result));
|
|
110
|
+
return lines.join("\n");
|
|
248
111
|
}
|
|
249
112
|
|
|
250
|
-
function
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
113
|
+
function resolveModels(ctx: Parameters<Parameters<ExtensionAPI["registerTool"]>[0]["execute"]>[4]) {
|
|
114
|
+
const config = loadReviewConfig(ctx.cwd);
|
|
115
|
+
const reviewer = resolveAgentReviewModel(ctx, config.agentModel);
|
|
116
|
+
const planner = resolveAgentReviewModel(ctx, config.plannerModel);
|
|
117
|
+
if (!reviewer)
|
|
118
|
+
throw new Error(`Configured reviewer model "${config.agentModel}" is unavailable.`);
|
|
119
|
+
return { reviewer, planner, config };
|
|
254
120
|
}
|
|
255
121
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Wraps onUpdate to also update a StatusSpinner with progress counts.
|
|
124
|
+
* Returns the adapted callback that both updates the spinner and calls onUpdate.
|
|
125
|
+
*/
|
|
126
|
+
function wireSpinnerToProgress(
|
|
127
|
+
// biome-ignore lint/suspicious/noExplicitAny: tool execute ctx type is narrower than ExtensionContext
|
|
128
|
+
ctx: any,
|
|
129
|
+
// biome-ignore lint/suspicious/noExplicitAny: onUpdate callback type varies per tool
|
|
130
|
+
onUpdate: any,
|
|
131
|
+
startMessage = "Reviewing…",
|
|
132
|
+
): {
|
|
133
|
+
statusSpinner: StatusSpinner;
|
|
134
|
+
wrappedUpdate: (result: {
|
|
135
|
+
content: Array<{ type: "text"; text: string }>;
|
|
136
|
+
details: Record<string, unknown>;
|
|
137
|
+
}) => void;
|
|
138
|
+
} {
|
|
139
|
+
const statusSpinner = new StatusSpinner(ctx, "supi-review");
|
|
140
|
+
statusSpinner.start(startMessage);
|
|
261
141
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
142
|
+
let completedCount = 0;
|
|
143
|
+
let totalCount = 0;
|
|
144
|
+
const wrappedUpdate = (result: {
|
|
145
|
+
content: Array<{ type: "text"; text: string }>;
|
|
146
|
+
details: Record<string, unknown>;
|
|
147
|
+
}) => {
|
|
148
|
+
const details = result.details as { completedCount?: number; totalCount?: number };
|
|
149
|
+
completedCount = details.completedCount ?? completedCount;
|
|
150
|
+
totalCount = details.totalCount ?? totalCount;
|
|
151
|
+
const label =
|
|
152
|
+
totalCount > 0
|
|
153
|
+
? `Reviewing… (${completedCount} of ${totalCount} tasks complete)`
|
|
154
|
+
: "Reviewing…";
|
|
155
|
+
statusSpinner.update(label);
|
|
156
|
+
onUpdate?.(result);
|
|
269
157
|
};
|
|
158
|
+
return { statusSpinner, wrappedUpdate };
|
|
270
159
|
}
|
|
271
160
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
161
|
+
/** Compute the initial task count for the progress indicator. */
|
|
162
|
+
function initialTaskCount(
|
|
163
|
+
input: ReturnType<typeof parseRunReviewToolInput>,
|
|
164
|
+
planStore: ReviewPlanStore,
|
|
165
|
+
): number {
|
|
166
|
+
if (input.mode === "direct") return input.review?.tasks?.length ?? 1;
|
|
167
|
+
if (input.decision?.kind === "use-review") return input.decision.review?.tasks?.length ?? 0;
|
|
168
|
+
if (input.decision?.kind === "accept-draft") {
|
|
169
|
+
return planStore.peek(input.planId)?.plannerDraft?.tasks.length ?? 0;
|
|
170
|
+
}
|
|
171
|
+
return 0;
|
|
276
172
|
}
|
|
277
173
|
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
)
|
|
174
|
+
/** Factory for the supi_review_run execute function with animated status-bar spinner. */
|
|
175
|
+
function makeRunReviewExecute(
|
|
176
|
+
planStore: ReviewPlanStore,
|
|
177
|
+
): NonNullable<Parameters<ExtensionAPI["registerTool"]>[0]["execute"]> {
|
|
178
|
+
// biome-ignore lint/complexity/useMaxParams: Pi ToolDefinition execute signature
|
|
179
|
+
return async (_id, params, signal, onUpdate, ctx) => {
|
|
180
|
+
const input = parseRunReviewToolInput(params);
|
|
181
|
+
|
|
182
|
+
const { statusSpinner, wrappedUpdate } = wireSpinnerToProgress(ctx, onUpdate);
|
|
183
|
+
|
|
184
|
+
const taskCount = initialTaskCount(input, planStore);
|
|
185
|
+
wrappedUpdate({
|
|
186
|
+
content: [{ type: "text", text: "Starting review…" }],
|
|
187
|
+
details: taskCount > 0 ? { completedCount: 0, totalCount: taskCount } : {},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const outcome =
|
|
192
|
+
input.mode === "direct"
|
|
193
|
+
? await runReview({
|
|
194
|
+
mode: "direct",
|
|
195
|
+
cwd: ctx.cwd,
|
|
196
|
+
target: input.target,
|
|
197
|
+
review: input.review,
|
|
198
|
+
reviewerModel: resolveModels(ctx).reviewer,
|
|
199
|
+
signal,
|
|
200
|
+
onUpdate: wrappedUpdate,
|
|
201
|
+
})
|
|
202
|
+
: await runReview({
|
|
203
|
+
mode: "prepared",
|
|
204
|
+
cwd: ctx.cwd,
|
|
205
|
+
planId: input.planId,
|
|
206
|
+
decision: input.decision,
|
|
207
|
+
planStore,
|
|
208
|
+
signal,
|
|
209
|
+
onUpdate: wrappedUpdate,
|
|
210
|
+
});
|
|
211
|
+
if (outcome.kind !== "completed") throw new Error(outcome.reason);
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: pageText(formatReviewBatch(outcome.details)).text }],
|
|
214
|
+
details: outcome.details,
|
|
215
|
+
};
|
|
216
|
+
} finally {
|
|
217
|
+
statusSpinner.stop();
|
|
218
|
+
}
|
|
285
219
|
};
|
|
286
220
|
}
|
|
287
221
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
222
|
+
/** Throw an actionable error from a non-prepared outcome. */
|
|
223
|
+
function throwPrepareFailure(
|
|
224
|
+
outcome: { kind: string; reason?: string } & Record<string, unknown>,
|
|
225
|
+
): never {
|
|
226
|
+
if (outcome.kind === "no-target")
|
|
227
|
+
throw new Error(outcome.reason ?? "No reviewable changes found.");
|
|
228
|
+
const result = outcome.result as { kind: string; failureCode?: string; timeoutMs?: number };
|
|
229
|
+
const diagnostic =
|
|
230
|
+
result.kind === "failed"
|
|
231
|
+
? result.failureCode
|
|
232
|
+
: result.kind === "timeout"
|
|
233
|
+
? `timeout (${result.timeoutMs} ms)`
|
|
234
|
+
: result.kind;
|
|
235
|
+
throw new Error(`Planner failed: ${diagnostic}`);
|
|
296
236
|
}
|
|
297
237
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
238
|
+
/** Register optional preparation and universal direct/prepared execution tools. */
|
|
239
|
+
export function registerAgentReviewTools(pi: ExtensionAPI, planStore: ReviewPlanStore): void {
|
|
240
|
+
pi.registerTool({
|
|
241
|
+
name: "supi_review_prepare",
|
|
242
|
+
label: "Prepare Review",
|
|
243
|
+
description: "Create a one-shot Review Plan, optionally with a lightweight Planner Draft.",
|
|
244
|
+
promptSnippet: "Prepare an optional one-shot code review plan",
|
|
245
|
+
promptGuidelines: [
|
|
246
|
+
"Use supi_review_prepare only when a caller wants to inspect or request a Planner Draft before execution.",
|
|
247
|
+
"Use supi_review_run in direct mode when the current skill or agent already knows the review tasks.",
|
|
248
|
+
],
|
|
249
|
+
parameters: prepareReviewSchema,
|
|
250
|
+
renderCall: renderPrepareCall,
|
|
251
|
+
renderResult: renderPrepareResult,
|
|
252
|
+
// biome-ignore lint/complexity/useMaxParams: Pi ToolDefinition execute signature
|
|
253
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
254
|
+
const input = parsePrepareReviewToolInput(params);
|
|
255
|
+
const models = resolveModels(ctx);
|
|
256
|
+
if ((input.planning ?? "none") === "suggest" && !models.planner) {
|
|
257
|
+
throw new Error(`Configured Planner model "${models.config.plannerModel}" is unavailable.`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const { statusSpinner, wrappedUpdate } = wireSpinnerToProgress(
|
|
261
|
+
ctx,
|
|
262
|
+
onUpdate,
|
|
263
|
+
"Preparing review…",
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const session = buildSessionContext(
|
|
268
|
+
ctx.sessionManager.getEntries(),
|
|
269
|
+
ctx.sessionManager.getLeafId(),
|
|
270
|
+
);
|
|
271
|
+
const outcome = await prepareReview({
|
|
272
|
+
cwd: ctx.cwd,
|
|
273
|
+
target: target(input.target),
|
|
274
|
+
planning: input.planning ?? "none",
|
|
275
|
+
plannerContext: collectPlannerContext(session.messages),
|
|
276
|
+
reviewerModel: models.reviewer,
|
|
277
|
+
plannerModel: models.planner,
|
|
278
|
+
planStore,
|
|
279
|
+
signal,
|
|
280
|
+
onUpdate: wrappedUpdate,
|
|
281
|
+
});
|
|
282
|
+
if (outcome.kind !== "prepared") throwPrepareFailure(outcome);
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: pageText(formatPrepared(outcome.plan)).text }],
|
|
285
|
+
details: {
|
|
286
|
+
kind: "review-prepared",
|
|
287
|
+
planId: outcome.plan.id,
|
|
288
|
+
snapshot: summarizeReviewSnapshot(outcome.plan.snapshot),
|
|
289
|
+
reviewerModelId: outcome.plan.reviewerModel.canonicalId,
|
|
290
|
+
plannerDraft: outcome.plan.plannerDraft,
|
|
291
|
+
plannerModelId: outcome.plan.plannerModelId,
|
|
292
|
+
plannerPromptVersion: outcome.plan.plannerPromptVersion,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
} finally {
|
|
296
|
+
statusSpinner.stop();
|
|
297
|
+
}
|
|
317
298
|
},
|
|
318
|
-
rawData: evaluation,
|
|
319
299
|
});
|
|
320
|
-
}
|
|
321
300
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
301
|
+
pi.registerTool({
|
|
302
|
+
name: "supi_review_run",
|
|
303
|
+
label: "Run Review",
|
|
304
|
+
description: "Run one to four caller-defined review tasks directly or from a prepared plan.",
|
|
305
|
+
promptSnippet: "Run independent read-only code review tasks",
|
|
306
|
+
promptGuidelines: [
|
|
307
|
+
"Use supi_review_run directly when a skill or the main agent already has complete review task instructions.",
|
|
308
|
+
"Repository stability from invocation through completion is a caller precondition for supi_review_run.",
|
|
309
|
+
],
|
|
310
|
+
parameters: runReviewSchema,
|
|
311
|
+
renderCall: renderRunCall,
|
|
312
|
+
renderResult: renderRunResult,
|
|
313
|
+
execute: makeRunReviewExecute(planStore),
|
|
326
314
|
});
|
|
327
|
-
if (!truncation.truncated) {
|
|
328
|
-
return { content: [{ type: "text" as const, text: truncation.content }], details };
|
|
329
|
-
}
|
|
330
315
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
writeFileSync(file, content, { encoding: "utf-8", mode: 0o600 });
|
|
334
|
-
const note =
|
|
335
|
-
`\n\n[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines ` +
|
|
336
|
-
`(${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). ` +
|
|
337
|
-
`Full output saved to: ${file}]`;
|
|
338
|
-
return {
|
|
339
|
-
content: [{ type: "text" as const, text: truncation.content + note }],
|
|
340
|
-
details,
|
|
341
|
-
};
|
|
316
|
+
pi.on("session_start", () => planStore.clear());
|
|
317
|
+
pi.on("session_shutdown", () => planStore.clear());
|
|
342
318
|
}
|