@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
package/src/review.ts
CHANGED
|
@@ -1,319 +1,220 @@
|
|
|
1
1
|
import { buildSessionContext, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import type {
|
|
14
|
-
BriefSynthesisRunResult,
|
|
15
|
-
RawReviewResult,
|
|
16
|
-
ReviewPlan,
|
|
17
|
-
ReviewResult,
|
|
18
|
-
ReviewSnapshot,
|
|
19
|
-
ReviewTargetSpec,
|
|
20
|
-
} from "./types.ts";
|
|
21
|
-
import { collectReviewNote, previewReviewPlan, selectModel, selectTarget } from "./ui/flow.ts";
|
|
22
|
-
import {
|
|
23
|
-
formatBriefSynthesisFailureContent,
|
|
24
|
-
formatBriefSynthesisFailureCopy,
|
|
25
|
-
formatReviewContent,
|
|
26
|
-
} from "./ui/format-content.ts";
|
|
27
|
-
import { registerReviewRenderer } from "./ui/renderer.ts";
|
|
2
|
+
import { Box } from "@earendil-works/pi-tui";
|
|
3
|
+
import { loadReviewConfig, registerReviewSettings } from "./config.ts";
|
|
4
|
+
import { listLocalBranches, listRecentCommits } from "./git.ts";
|
|
5
|
+
import { collectPlannerContext } from "./history/collect.ts";
|
|
6
|
+
import { getSelectableReviewModels, resolveAgentReviewModel } from "./model.ts";
|
|
7
|
+
import { ReviewPlanStore } from "./session/review-plan-store.ts";
|
|
8
|
+
import { formatReviewBatch, registerAgentReviewTools } from "./tool/agent-review-tools.ts";
|
|
9
|
+
import { pageText } from "./tool/output-page.ts";
|
|
10
|
+
import { prepareReview, runReview } from "./tool/review-workflow.ts";
|
|
11
|
+
import { renderRunResult } from "./tui/run.ts";
|
|
12
|
+
import type { ReviewInput, ReviewModelSelection, ReviewTargetSpec } from "./types.ts";
|
|
28
13
|
|
|
29
14
|
type CommandContext = Parameters<Parameters<ExtensionAPI["registerCommand"]>[1]["handler"]>[1];
|
|
30
15
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
pi.registerCommand("supi-review", {
|
|
37
|
-
description: "Run a structured code review informed by the current session history",
|
|
38
|
-
handler: async (_args, ctx) => {
|
|
39
|
-
await handleInteractive(ctx, pi);
|
|
16
|
+
const DEFAULT_REVIEW: ReviewInput = {
|
|
17
|
+
tasks: [
|
|
18
|
+
{
|
|
19
|
+
id: "general",
|
|
20
|
+
instructions: "Review for concrete regressions introduced by this change.",
|
|
40
21
|
},
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const normalizedNote = note.trim() || undefined;
|
|
58
|
-
|
|
59
|
-
const snapshot = await resolveReviewSnapshot(target, ctx);
|
|
60
|
-
if (!snapshot) return;
|
|
61
|
-
|
|
62
|
-
const sessionContext = buildSessionContext(
|
|
63
|
-
ctx.sessionManager.getEntries(),
|
|
64
|
-
ctx.sessionManager.getLeafId(),
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function selectTarget(ctx: CommandContext): Promise<ReviewTargetSpec | undefined> {
|
|
26
|
+
const kind = await ctx.ui.select("Review target", [
|
|
27
|
+
"Working tree",
|
|
28
|
+
"Comparison against a base commit",
|
|
29
|
+
"Single commit",
|
|
30
|
+
]);
|
|
31
|
+
if (!kind) return undefined;
|
|
32
|
+
if (kind === "Working tree") return { kind: "working-tree" };
|
|
33
|
+
const choices =
|
|
34
|
+
kind === "Single commit" ? await listRecentCommits(ctx.cwd) : await listLocalBranches(ctx.cwd);
|
|
35
|
+
const label = await ctx.ui.select(
|
|
36
|
+
kind === "Single commit" ? "Commit" : "Base branch",
|
|
37
|
+
choices.map((choice) => choice.label),
|
|
65
38
|
);
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
synthesizeReviewBrief({
|
|
73
|
-
model,
|
|
74
|
-
cwd: ctx.cwd,
|
|
75
|
-
snapshot,
|
|
76
|
-
serializedContext,
|
|
77
|
-
note: normalizedNote,
|
|
78
|
-
signal,
|
|
79
|
-
onProgress,
|
|
80
|
-
}),
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
if (!synthesis) {
|
|
84
|
-
const failure: BriefSynthesisRunResult = {
|
|
85
|
-
kind: "failed",
|
|
86
|
-
failureCode: "unexpected-runner-failure",
|
|
87
|
-
diagnostics: createUnobservedChildFailureDiagnostics(),
|
|
88
|
-
};
|
|
89
|
-
notifyBriefDone(pi, failure, snapshot, model.canonicalId);
|
|
90
|
-
injectBriefSynthesisFailureMessage(pi, failure, snapshot, model.canonicalId);
|
|
91
|
-
ctx.ui.notify(formatBriefSynthesisFailureCopy(failure), "warning");
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
notifyBriefDone(pi, synthesis, snapshot, model.canonicalId);
|
|
39
|
+
const choice = choices.find((candidate) => candidate.label === label);
|
|
40
|
+
if (!choice) return undefined;
|
|
41
|
+
return kind === "Single commit"
|
|
42
|
+
? { kind: "commit", commit: choice.commit }
|
|
43
|
+
: { kind: "comparison", baseCommit: choice.commit };
|
|
44
|
+
}
|
|
96
45
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
46
|
+
async function selectReviewerModel(ctx: CommandContext): Promise<ReviewModelSelection | undefined> {
|
|
47
|
+
const models = getSelectableReviewModels(ctx);
|
|
48
|
+
if (models.length === 0) {
|
|
49
|
+
ctx.ui.notify("No scoped reviewer models are available.", "error");
|
|
50
|
+
return undefined;
|
|
101
51
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
note: normalizedNote,
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const packet = buildReviewPacket(snapshot, brief, model);
|
|
109
|
-
const plan: ReviewPlan = { model, snapshot, brief, packet };
|
|
110
|
-
|
|
111
|
-
const approved = await previewReviewPlan(ctx, plan);
|
|
112
|
-
if (!approved) return;
|
|
113
|
-
|
|
114
|
-
const rawResult = await runWithProgressWidget(
|
|
115
|
-
pi,
|
|
116
|
-
ctx,
|
|
117
|
-
"Running code review…",
|
|
118
|
-
(signal: AbortSignal, onProgress: (p: WidgetProgress) => void) =>
|
|
119
|
-
runReviewer({
|
|
120
|
-
prompt: plan.packet.prompt,
|
|
121
|
-
model: plan.model,
|
|
122
|
-
cwd: ctx.cwd,
|
|
123
|
-
signal,
|
|
124
|
-
snapshot: plan.snapshot,
|
|
125
|
-
brief: plan.brief,
|
|
126
|
-
onProgress,
|
|
127
|
-
}),
|
|
52
|
+
const id = await ctx.ui.select(
|
|
53
|
+
"Reviewer model",
|
|
54
|
+
models.map((model) => model.canonicalId),
|
|
128
55
|
);
|
|
56
|
+
return models.find((model) => model.canonicalId === id);
|
|
57
|
+
}
|
|
129
58
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return;
|
|
59
|
+
/** Edit one task's instructions via a focused editor. Returns undefined on cancel. */
|
|
60
|
+
async function editTaskInstructions(
|
|
61
|
+
ctx: CommandContext,
|
|
62
|
+
opts: { index: number; taskCount: number; id: string; defaultInstructions: string },
|
|
63
|
+
): Promise<string | undefined> {
|
|
64
|
+
const { index, taskCount, id, defaultInstructions } = opts;
|
|
65
|
+
const label = taskCount > 1 ? `Task ${index + 1} of ${taskCount}: ${id}` : `Task: ${id}`;
|
|
66
|
+
const instructions = await ctx.ui.editor(label, defaultInstructions);
|
|
67
|
+
if (instructions === undefined) return undefined;
|
|
68
|
+
if (!instructions.trim()) {
|
|
69
|
+
ctx.ui.notify("Task instructions must not be blank.", "error");
|
|
70
|
+
return undefined;
|
|
143
71
|
}
|
|
144
|
-
|
|
145
|
-
const result = normalizeReviewResult(rawResult as RawReviewResult);
|
|
146
|
-
notifyReviewDone(pi, result);
|
|
147
|
-
injectReviewMessage(pi, result);
|
|
72
|
+
return instructions.trim();
|
|
148
73
|
}
|
|
149
74
|
|
|
150
|
-
|
|
151
|
-
|
|
75
|
+
/** Step-by-step wizard: edit each task's instructions in its own focused editor. */
|
|
76
|
+
async function editReviewInteractive(
|
|
152
77
|
ctx: CommandContext,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return snapshot;
|
|
78
|
+
review: ReviewInput,
|
|
79
|
+
{ allowResize }: { allowResize: boolean },
|
|
80
|
+
): Promise<ReviewInput | undefined> {
|
|
81
|
+
let taskCount = review.tasks.length;
|
|
82
|
+
|
|
83
|
+
if (allowResize) {
|
|
84
|
+
const countStr = await ctx.ui.select("How many review tasks?", ["1", "2", "3", "4"]);
|
|
85
|
+
if (!countStr) return undefined;
|
|
86
|
+
taskCount = Number(countStr);
|
|
163
87
|
}
|
|
164
88
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
89
|
+
const tasks: ReviewInput["tasks"] = [];
|
|
90
|
+
for (let i = 0; i < taskCount; i++) {
|
|
91
|
+
const existing = review.tasks[i];
|
|
92
|
+
const id = existing?.id ?? `task-${i + 1}`;
|
|
93
|
+
const defaultInstructions = existing?.instructions ?? "";
|
|
94
|
+
|
|
95
|
+
const instructions = await editTaskInstructions(ctx, {
|
|
96
|
+
index: i,
|
|
97
|
+
taskCount,
|
|
98
|
+
id,
|
|
99
|
+
defaultInstructions,
|
|
100
|
+
});
|
|
101
|
+
if (instructions === undefined) return undefined;
|
|
102
|
+
tasks.push({ id, instructions });
|
|
175
103
|
}
|
|
176
104
|
|
|
177
|
-
|
|
105
|
+
const result: ReviewInput = { tasks };
|
|
106
|
+
if (review.sharedContext) {
|
|
107
|
+
const sharedContext = await ctx.ui.editor("Shared context (optional)", review.sharedContext);
|
|
108
|
+
if (sharedContext === undefined) return undefined;
|
|
109
|
+
if (sharedContext.trim()) result.sharedContext = sharedContext.trim();
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
178
112
|
}
|
|
179
113
|
|
|
180
|
-
function
|
|
181
|
-
result: Exclude<BriefSynthesisRunResult, { kind: "success" }>,
|
|
114
|
+
async function runCommand(
|
|
182
115
|
ctx: CommandContext,
|
|
183
|
-
): void {
|
|
184
|
-
ctx.ui.notify(
|
|
185
|
-
formatBriefSynthesisFailureCopy(result),
|
|
186
|
-
result.kind === "failed" ? "error" : "warning",
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Persist a brief-synthesis failure so bounded diagnostics remain inspectable in parent history. */
|
|
191
|
-
function injectBriefSynthesisFailureMessage(
|
|
192
|
-
pi: ExtensionAPI,
|
|
193
|
-
result: Exclude<BriefSynthesisRunResult, { kind: "success" }>,
|
|
194
|
-
snapshot: ReviewSnapshot,
|
|
195
|
-
modelId: string,
|
|
196
|
-
): void {
|
|
197
|
-
pi.sendMessage({
|
|
198
|
-
customType: "supi-review",
|
|
199
|
-
content: formatBriefSynthesisFailureContent(result),
|
|
200
|
-
display: true,
|
|
201
|
-
details: {
|
|
202
|
-
briefSynthesisFailure: { result, snapshot, modelId },
|
|
203
|
-
},
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Ring the terminal bell so the user knows to check back. */
|
|
208
|
-
function ringBell(): void {
|
|
209
|
-
process.stdout.write("\x07");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Emit a `supi:review:brief-done` event and ring the terminal bell after
|
|
214
|
-
* brief synthesis completes (success, failure, cancel, or timeout).
|
|
215
|
-
*/
|
|
216
|
-
function notifyBriefDone(
|
|
217
116
|
pi: ExtensionAPI,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
117
|
+
planStore: ReviewPlanStore,
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
if (!ctx.hasUI) return;
|
|
120
|
+
const target = await selectTarget(ctx);
|
|
121
|
+
if (!target) return;
|
|
122
|
+
const reviewerModel = await selectReviewerModel(ctx);
|
|
123
|
+
if (!reviewerModel) return;
|
|
124
|
+
const planning = await ctx.ui.select("Review planning", [
|
|
125
|
+
"Write my own tasks",
|
|
126
|
+
"AI suggests tasks",
|
|
127
|
+
]);
|
|
128
|
+
if (!planning) return;
|
|
129
|
+
|
|
130
|
+
let review = DEFAULT_REVIEW;
|
|
131
|
+
let planId: string | undefined;
|
|
132
|
+
if (planning === "AI suggests tasks") {
|
|
133
|
+
const config = loadReviewConfig(ctx.cwd);
|
|
134
|
+
const plannerModel = resolveAgentReviewModel(ctx, config.plannerModel);
|
|
135
|
+
if (!plannerModel) {
|
|
136
|
+
ctx.ui.notify(`Configured Planner model "${config.plannerModel}" is unavailable.`, "error");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const session = buildSessionContext(
|
|
140
|
+
ctx.sessionManager.getEntries(),
|
|
141
|
+
ctx.sessionManager.getLeafId(),
|
|
142
|
+
);
|
|
143
|
+
const outcome = await prepareReview({
|
|
144
|
+
cwd: ctx.cwd,
|
|
145
|
+
target,
|
|
146
|
+
planning: "suggest",
|
|
147
|
+
plannerContext: collectPlannerContext(session.messages),
|
|
148
|
+
reviewerModel,
|
|
149
|
+
plannerModel,
|
|
150
|
+
planStore,
|
|
151
|
+
});
|
|
152
|
+
if (outcome.kind !== "prepared" || !outcome.plan.plannerDraft) {
|
|
153
|
+
ctx.ui.notify("Planner did not produce a draft.", "error");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
review = outcome.plan.plannerDraft;
|
|
157
|
+
planId = outcome.plan.id;
|
|
158
|
+
}
|
|
230
159
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
* the review child session completes.
|
|
234
|
-
*/
|
|
235
|
-
function notifyReviewDone(pi: ExtensionAPI, result: ReviewResult): void {
|
|
236
|
-
pi.events.emit("supi:review:review-done", {
|
|
237
|
-
kind: result.kind,
|
|
238
|
-
snapshot: result.snapshot.title,
|
|
239
|
-
modelId: result.modelId,
|
|
240
|
-
itemCount: result.kind === "success" ? result.output.items.length : 0,
|
|
160
|
+
const edited = await editReviewInteractive(ctx, review, {
|
|
161
|
+
allowResize: planning === "Write my own tasks",
|
|
241
162
|
});
|
|
242
|
-
|
|
243
|
-
|
|
163
|
+
if (!edited) return;
|
|
164
|
+
const approved = await ctx.ui.confirm(
|
|
165
|
+
"Run review?",
|
|
166
|
+
`${edited.tasks.length} task(s) using ${reviewerModel.canonicalId}`,
|
|
167
|
+
);
|
|
168
|
+
if (!approved) return;
|
|
244
169
|
|
|
245
|
-
|
|
170
|
+
const outcome = planId
|
|
171
|
+
? await runReview({
|
|
172
|
+
mode: "prepared",
|
|
173
|
+
cwd: ctx.cwd,
|
|
174
|
+
planId,
|
|
175
|
+
decision: { kind: "use-review", review: edited },
|
|
176
|
+
planStore,
|
|
177
|
+
})
|
|
178
|
+
: await runReview({
|
|
179
|
+
mode: "direct",
|
|
180
|
+
cwd: ctx.cwd,
|
|
181
|
+
target,
|
|
182
|
+
review: edited,
|
|
183
|
+
reviewerModel,
|
|
184
|
+
});
|
|
185
|
+
if (outcome.kind !== "completed") {
|
|
186
|
+
ctx.ui.notify(outcome.reason, "error");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
246
189
|
pi.sendMessage({
|
|
247
190
|
customType: "supi-review",
|
|
248
|
-
content:
|
|
191
|
+
content: pageText(formatReviewBatch(outcome.details)).text,
|
|
249
192
|
display: true,
|
|
250
|
-
details:
|
|
193
|
+
details: outcome.details,
|
|
251
194
|
});
|
|
252
|
-
|
|
253
|
-
maybeQueueReviewFollowUp(pi, result);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function maybeQueueReviewFollowUp(pi: ExtensionAPI, result: ReviewResult): void {
|
|
257
|
-
if (result.kind !== "success" || result.output.items.length === 0) {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
pi.sendMessage(
|
|
262
|
-
{
|
|
263
|
-
customType: "supi-review-followup",
|
|
264
|
-
content: buildReviewFollowUpInstruction(result),
|
|
265
|
-
display: false,
|
|
266
|
-
details: {
|
|
267
|
-
itemCount: result.output.items.length,
|
|
268
|
-
actionSummary: result.output.summary.actions,
|
|
269
|
-
items: result.output.items.map((item, index) => ({
|
|
270
|
-
number: index + 1,
|
|
271
|
-
title: item.title,
|
|
272
|
-
recommended_action: item.recommended_action,
|
|
273
|
-
})),
|
|
274
|
-
},
|
|
275
|
-
},
|
|
276
|
-
{ triggerTurn: true },
|
|
277
|
-
);
|
|
278
195
|
}
|
|
279
196
|
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (summary.actions.mustFix > 0) {
|
|
299
|
-
lines.push(`${summary.actions.mustFix} must-fix item(s) — fix before merging.`);
|
|
300
|
-
} else if (summary.actions.shouldFix > 0) {
|
|
301
|
-
lines.push(
|
|
302
|
-
`${summary.actions.shouldFix} should-fix item(s) — review carefully before proceeding.`,
|
|
303
|
-
);
|
|
304
|
-
} else if (summary.actions.consider > 0) {
|
|
305
|
-
lines.push(`${summary.actions.consider} consider item(s) — optional follow-ups to review.`);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
lines.push(noFixing, useAskUser);
|
|
309
|
-
lines.push("Offer these options: Fix all, Fix selected, Verify findings, Skip.");
|
|
310
|
-
lines.push(
|
|
311
|
-
"If the user chooses Fix selected, ask a follow-up question listing the review items by number/title.",
|
|
312
|
-
);
|
|
313
|
-
lines.push(
|
|
314
|
-
"If the user chooses Verify findings, re-read the relevant files and diffs to independently confirm or refute each review item, then present the verified results and ask again.",
|
|
315
|
-
);
|
|
316
|
-
lines.push("", "Current review items:", ...itemList);
|
|
197
|
+
export default function reviewExtension(pi: ExtensionAPI): void {
|
|
198
|
+
const planStore = new ReviewPlanStore();
|
|
199
|
+
registerReviewSettings(pi);
|
|
200
|
+
registerAgentReviewTools(pi, planStore);
|
|
201
|
+
|
|
202
|
+
// Message renderer for the /supi-review slash command output.
|
|
203
|
+
// Adapts the sendMessage shape into the shape renderRunResult expects.
|
|
204
|
+
pi.registerMessageRenderer("supi-review", (message, options, theme) => {
|
|
205
|
+
const adapted = {
|
|
206
|
+
content: [{ type: "text" as const, text: message.content as string }],
|
|
207
|
+
details: message.details,
|
|
208
|
+
isError: false,
|
|
209
|
+
};
|
|
210
|
+
const result = renderRunResult(adapted, { ...options, isPartial: false }, theme);
|
|
211
|
+
const box = new Box(options.outputPad, 0, undefined);
|
|
212
|
+
box.addChild(result);
|
|
213
|
+
return box;
|
|
214
|
+
});
|
|
317
215
|
|
|
318
|
-
|
|
216
|
+
pi.registerCommand("supi-review", {
|
|
217
|
+
description: "Run one or more caller-defined read-only review tasks",
|
|
218
|
+
handler: async (_args, ctx) => runCommand(ctx, pi, planStore),
|
|
219
|
+
});
|
|
319
220
|
}
|
|
@@ -1,69 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { ReviewModelSelection, ReviewSnapshot
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { PlannerDraft, ReviewModelSelection, ReviewSnapshot } from "../types.ts";
|
|
3
3
|
|
|
4
|
-
/** Session-
|
|
5
|
-
export interface
|
|
4
|
+
/** Session-local resolved target and model choices awaiting one execution decision. */
|
|
5
|
+
export interface StoredReviewPlan {
|
|
6
6
|
id: string;
|
|
7
7
|
snapshot: ReviewSnapshot;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
createdAt: number;
|
|
8
|
+
reviewerModel: ReviewModelSelection;
|
|
9
|
+
plannerDraft?: PlannerDraft;
|
|
10
|
+
plannerModelId?: string;
|
|
11
|
+
plannerPromptVersion?: string;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
/**
|
|
16
|
-
export interface CreateAgentReviewPlanInput {
|
|
17
|
-
snapshot: ReviewSnapshot;
|
|
18
|
-
snapshotFingerprint: string;
|
|
19
|
-
generatedBrief: SynthesizedReviewBrief;
|
|
20
|
-
model: ReviewModelSelection;
|
|
21
|
-
briefPromptVersion: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Owns prepared review plans for one PI extension session.
|
|
26
|
-
*
|
|
27
|
-
* Plans are claimed atomically before reviewer child sessions start, preventing
|
|
28
|
-
* duplicate runs from concurrent sibling tool calls.
|
|
29
|
-
*/
|
|
14
|
+
/** In-memory one-shot store; `take` atomically removes a plan before validation/execution. */
|
|
30
15
|
export class ReviewPlanStore {
|
|
31
|
-
readonly #plans = new Map<string,
|
|
32
|
-
#sequence = 0;
|
|
16
|
+
readonly #plans = new Map<string, StoredReviewPlan>();
|
|
33
17
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const id = this.createPlanId(input.snapshotFingerprint, createdAt);
|
|
38
|
-
const plan: StoredAgentReviewPlan = { id, createdAt, ...input };
|
|
39
|
-
this.#plans.set(id, plan);
|
|
18
|
+
create(input: Omit<StoredReviewPlan, "id">): StoredReviewPlan {
|
|
19
|
+
const plan = { id: `review-plan-${randomUUID()}`, ...input };
|
|
20
|
+
this.#plans.set(plan.id, plan);
|
|
40
21
|
return plan;
|
|
41
22
|
}
|
|
42
23
|
|
|
43
|
-
|
|
44
|
-
get(id: string): StoredAgentReviewPlan | undefined {
|
|
45
|
-
return this.#plans.get(id);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Atomically consume a plan so it can run at most once. */
|
|
49
|
-
take(id: string): StoredAgentReviewPlan | undefined {
|
|
24
|
+
take(id: string): StoredReviewPlan | undefined {
|
|
50
25
|
const plan = this.#plans.get(id);
|
|
51
|
-
if (
|
|
52
|
-
this.#plans.delete(id);
|
|
26
|
+
if (plan) this.#plans.delete(id);
|
|
53
27
|
return plan;
|
|
54
28
|
}
|
|
55
29
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
this.#plans.
|
|
30
|
+
/** Read a plan without consuming it, for pre-execution task count display. */
|
|
31
|
+
peek(id: string): StoredReviewPlan | undefined {
|
|
32
|
+
return this.#plans.get(id);
|
|
59
33
|
}
|
|
60
34
|
|
|
61
|
-
|
|
62
|
-
this.#
|
|
63
|
-
const hash = createHash("sha256")
|
|
64
|
-
.update(`${snapshotFingerprint}:${createdAt}:${this.#sequence}`)
|
|
65
|
-
.digest("hex")
|
|
66
|
-
.slice(0, 12);
|
|
67
|
-
return `review-plan-${hash}`;
|
|
35
|
+
clear(): void {
|
|
36
|
+
this.#plans.clear();
|
|
68
37
|
}
|
|
69
38
|
}
|