@mrclrchtr/supi-review 2.7.0 → 3.0.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 +75 -163
- package/node_modules/@mrclrchtr/supi-core/README.md +1 -1
- 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/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +1 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +14 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +31 -14
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
- package/package.json +3 -3
- package/src/config.ts +64 -0
- package/src/git-command.ts +78 -0
- package/src/git.ts +306 -505
- package/src/history/collect.ts +43 -120
- package/src/model.ts +35 -0
- package/src/review-path.ts +85 -0
- package/src/review-result.ts +43 -92
- package/src/review.ts +154 -288
- package/src/session/review-plan-store.ts +20 -51
- package/src/target/packet.ts +46 -369
- package/src/tool/agent-review-schemas.ts +101 -104
- package/src/tool/agent-review-tools.ts +269 -301
- package/src/tool/child-failure-diagnostics.ts +1 -1
- 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 +309 -0
- package/src/tool/runner-helpers.ts +3 -8
- package/src/tool/schemas.ts +75 -62
- package/src/tui/common.ts +175 -0
- package/src/tui/prepare.ts +132 -0
- package/src/tui/run.ts +160 -0
- package/src/types.ts +153 -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,317 +1,183 @@
|
|
|
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 { registerAgentReviewTools } from "./tool/agent-review-tools.ts";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
ReviewPlan,
|
|
16
|
-
ReviewResult,
|
|
17
|
-
ReviewSnapshot,
|
|
18
|
-
ReviewTargetSpec,
|
|
19
|
-
} from "./types.ts";
|
|
20
|
-
import { collectReviewNote, previewReviewPlan, selectModel, selectTarget } from "./ui/flow.ts";
|
|
21
|
-
import {
|
|
22
|
-
formatBriefSynthesisFailureContent,
|
|
23
|
-
formatBriefSynthesisFailureCopy,
|
|
24
|
-
formatReviewContent,
|
|
25
|
-
} from "./ui/format-content.ts";
|
|
26
|
-
import { registerReviewRenderer } from "./ui/renderer.ts";
|
|
2
|
+
import { Box } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
4
|
+
import { loadReviewConfig, registerReviewSettings } from "./config.ts";
|
|
5
|
+
import { listLocalBranches, listRecentCommits } from "./git.ts";
|
|
6
|
+
import { collectPlannerContext } from "./history/collect.ts";
|
|
7
|
+
import { getSelectableReviewModels, resolveAgentReviewModel } from "./model.ts";
|
|
8
|
+
import { ReviewPlanStore } from "./session/review-plan-store.ts";
|
|
9
|
+
import { formatReviewBatch, registerAgentReviewTools } from "./tool/agent-review-tools.ts";
|
|
10
|
+
import { pageText } from "./tool/output-page.ts";
|
|
11
|
+
import { normalizeReviewInput, prepareReview, runReview } from "./tool/review-workflow.ts";
|
|
12
|
+
import { reviewInputSchema } from "./tool/schemas.ts";
|
|
13
|
+
import { renderRunResult } from "./tui/run.ts";
|
|
14
|
+
import type { ReviewInput, ReviewModelSelection, ReviewTargetSpec } from "./types.ts";
|
|
27
15
|
|
|
28
16
|
type CommandContext = Parameters<Parameters<ExtensionAPI["registerCommand"]>[1]["handler"]>[1];
|
|
29
17
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
description: "Run a structured code review informed by the current session history",
|
|
36
|
-
handler: async (_args, ctx) => {
|
|
37
|
-
await handleInteractive(ctx, pi);
|
|
18
|
+
const DEFAULT_REVIEW: ReviewInput = {
|
|
19
|
+
tasks: [
|
|
20
|
+
{
|
|
21
|
+
id: "general",
|
|
22
|
+
instructions: "Review for concrete regressions introduced by this change.",
|
|
38
23
|
},
|
|
39
|
-
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
async function selectTarget(ctx: CommandContext): Promise<ReviewTargetSpec | undefined> {
|
|
28
|
+
const kind = await ctx.ui.select("Review target", [
|
|
29
|
+
"Working tree",
|
|
30
|
+
"Comparison against a base commit",
|
|
31
|
+
"Single commit",
|
|
32
|
+
]);
|
|
33
|
+
if (!kind) return undefined;
|
|
34
|
+
if (kind === "Working tree") return { kind: "working-tree" };
|
|
35
|
+
const choices =
|
|
36
|
+
kind === "Single commit" ? await listRecentCommits(ctx.cwd) : await listLocalBranches(ctx.cwd);
|
|
37
|
+
const label = await ctx.ui.select(
|
|
38
|
+
kind === "Single commit" ? "Commit" : "Base branch",
|
|
39
|
+
choices.map((choice) => choice.label),
|
|
40
|
+
);
|
|
41
|
+
const choice = choices.find((candidate) => candidate.label === label);
|
|
42
|
+
if (!choice) return undefined;
|
|
43
|
+
return kind === "Single commit"
|
|
44
|
+
? { kind: "commit", commit: choice.commit }
|
|
45
|
+
: { kind: "comparison", baseCommit: choice.commit };
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
async function
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
async function selectReviewerModel(ctx: CommandContext): Promise<ReviewModelSelection | undefined> {
|
|
49
|
+
const models = getSelectableReviewModels(ctx);
|
|
50
|
+
if (models.length === 0) {
|
|
51
|
+
ctx.ui.notify("No scoped reviewer models are available.", "error");
|
|
52
|
+
return undefined;
|
|
45
53
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const model = await selectModel(ctx);
|
|
51
|
-
if (!model) return;
|
|
52
|
-
|
|
53
|
-
const note = await collectReviewNote(ctx);
|
|
54
|
-
if (note === undefined) return;
|
|
55
|
-
const normalizedNote = note.trim() || undefined;
|
|
56
|
-
|
|
57
|
-
const snapshot = await resolveReviewSnapshot(target, ctx);
|
|
58
|
-
if (!snapshot) return;
|
|
59
|
-
|
|
60
|
-
const sessionContext = buildSessionContext(
|
|
61
|
-
ctx.sessionManager.getEntries(),
|
|
62
|
-
ctx.sessionManager.getLeafId(),
|
|
63
|
-
);
|
|
64
|
-
const serializedContext = serializeSessionContext(sessionContext.messages);
|
|
65
|
-
const synthesis = await runWithProgressWidget(
|
|
66
|
-
pi,
|
|
67
|
-
ctx,
|
|
68
|
-
"Synthesizing review brief…",
|
|
69
|
-
(signal: AbortSignal, onProgress: (p: WidgetProgress) => void) =>
|
|
70
|
-
synthesizeReviewBrief({
|
|
71
|
-
model,
|
|
72
|
-
cwd: ctx.cwd,
|
|
73
|
-
snapshot,
|
|
74
|
-
serializedContext,
|
|
75
|
-
note: normalizedNote,
|
|
76
|
-
signal,
|
|
77
|
-
onProgress,
|
|
78
|
-
}),
|
|
54
|
+
const id = await ctx.ui.select(
|
|
55
|
+
"Reviewer model",
|
|
56
|
+
models.map((model) => model.canonicalId),
|
|
79
57
|
);
|
|
58
|
+
return models.find((model) => model.canonicalId === id);
|
|
59
|
+
}
|
|
80
60
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
61
|
+
/** Parse editor JSON through the same structural and semantic contract as agent input. */
|
|
62
|
+
export async function editReview(
|
|
63
|
+
ctx: CommandContext,
|
|
64
|
+
review: ReviewInput,
|
|
65
|
+
): Promise<ReviewInput | undefined> {
|
|
66
|
+
const text = await ctx.ui.editor("Edit review tasks (JSON)", JSON.stringify(review, null, 2));
|
|
67
|
+
if (text === undefined) return undefined;
|
|
68
|
+
try {
|
|
69
|
+
const parsed: unknown = JSON.parse(text);
|
|
70
|
+
if (!Value.Check(reviewInputSchema, parsed)) {
|
|
71
|
+
ctx.ui.notify("Review tasks do not match the required review schema.", "error");
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return normalizeReviewInput(parsed);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : "Review tasks must be valid JSON.";
|
|
77
|
+
ctx.ui.notify(message, "error");
|
|
78
|
+
return undefined;
|
|
91
79
|
}
|
|
80
|
+
}
|
|
92
81
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
async function runCommand(
|
|
83
|
+
ctx: CommandContext,
|
|
84
|
+
pi: ExtensionAPI,
|
|
85
|
+
planStore: ReviewPlanStore,
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
if (!ctx.hasUI) return;
|
|
88
|
+
const target = await selectTarget(ctx);
|
|
89
|
+
if (!target) return;
|
|
90
|
+
const reviewerModel = await selectReviewerModel(ctx);
|
|
91
|
+
if (!reviewerModel) return;
|
|
92
|
+
const planning = await ctx.ui.select("Review planning", ["Write tasks", "Suggest tasks"]);
|
|
93
|
+
if (!planning) return;
|
|
94
|
+
|
|
95
|
+
let review = DEFAULT_REVIEW;
|
|
96
|
+
let planId: string | undefined;
|
|
97
|
+
if (planning === "Suggest tasks") {
|
|
98
|
+
const config = loadReviewConfig(ctx.cwd);
|
|
99
|
+
const plannerModel = resolveAgentReviewModel(ctx, config.plannerModel);
|
|
100
|
+
if (!plannerModel) {
|
|
101
|
+
ctx.ui.notify(`Configured Planner model "${config.plannerModel}" is unavailable.`, "error");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const session = buildSessionContext(
|
|
105
|
+
ctx.sessionManager.getEntries(),
|
|
106
|
+
ctx.sessionManager.getLeafId(),
|
|
107
|
+
);
|
|
108
|
+
const outcome = await prepareReview({
|
|
109
|
+
cwd: ctx.cwd,
|
|
110
|
+
target,
|
|
111
|
+
planning: "suggest",
|
|
112
|
+
plannerContext: collectPlannerContext(session.messages),
|
|
113
|
+
reviewerModel,
|
|
114
|
+
plannerModel,
|
|
115
|
+
planStore,
|
|
116
|
+
});
|
|
117
|
+
if (outcome.kind !== "prepared" || !outcome.plan.plannerDraft) {
|
|
118
|
+
ctx.ui.notify("Planner did not produce a draft.", "error");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
review = outcome.plan.plannerDraft;
|
|
122
|
+
planId = outcome.plan.id;
|
|
99
123
|
}
|
|
100
124
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const plan: ReviewPlan = { model, snapshot, brief, packet };
|
|
108
|
-
|
|
109
|
-
const approved = await previewReviewPlan(ctx, plan);
|
|
125
|
+
const edited = await editReview(ctx, review);
|
|
126
|
+
if (!edited) return;
|
|
127
|
+
const approved = await ctx.ui.confirm(
|
|
128
|
+
"Run review?",
|
|
129
|
+
`${edited.tasks.length} task(s) using ${reviewerModel.canonicalId}`,
|
|
130
|
+
);
|
|
110
131
|
if (!approved) return;
|
|
111
132
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"Running code review…",
|
|
116
|
-
(signal: AbortSignal, onProgress: (p: WidgetProgress) => void) =>
|
|
117
|
-
runReviewer({
|
|
118
|
-
prompt: plan.packet.prompt,
|
|
119
|
-
model: plan.model,
|
|
133
|
+
const outcome = planId
|
|
134
|
+
? await runReview({
|
|
135
|
+
mode: "prepared",
|
|
120
136
|
cwd: ctx.cwd,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
brief,
|
|
135
|
-
modelId: model.canonicalId,
|
|
136
|
-
};
|
|
137
|
-
notifyReviewDone(pi, failure);
|
|
138
|
-
injectReviewMessage(pi, failure);
|
|
139
|
-
ctx.ui.notify("Reviewer ended unexpectedly.", "warning");
|
|
137
|
+
planId,
|
|
138
|
+
decision: { kind: "use-review", review: edited },
|
|
139
|
+
planStore,
|
|
140
|
+
})
|
|
141
|
+
: await runReview({
|
|
142
|
+
mode: "direct",
|
|
143
|
+
cwd: ctx.cwd,
|
|
144
|
+
target,
|
|
145
|
+
review: edited,
|
|
146
|
+
reviewerModel,
|
|
147
|
+
});
|
|
148
|
+
if (outcome.kind !== "completed") {
|
|
149
|
+
ctx.ui.notify(outcome.reason, "error");
|
|
140
150
|
return;
|
|
141
151
|
}
|
|
142
|
-
|
|
143
|
-
const result = normalizeReviewResult(rawResult as RawReviewResult);
|
|
144
|
-
notifyReviewDone(pi, result);
|
|
145
|
-
injectReviewMessage(pi, result);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function resolveReviewSnapshot(
|
|
149
|
-
target: ReviewTargetSpec,
|
|
150
|
-
ctx: CommandContext,
|
|
151
|
-
): Promise<ReviewSnapshot | undefined> {
|
|
152
|
-
const snapshot =
|
|
153
|
-
target.kind === "working-tree"
|
|
154
|
-
? await resolveWorkingTreeSnapshot(ctx.cwd)
|
|
155
|
-
: target.kind === "branch"
|
|
156
|
-
? await resolveBranchSnapshot(ctx.cwd, target.base)
|
|
157
|
-
: await resolveCommitSnapshot(ctx.cwd, target.sha);
|
|
158
|
-
|
|
159
|
-
if (snapshot) {
|
|
160
|
-
return snapshot;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
switch (target.kind) {
|
|
164
|
-
case "working-tree":
|
|
165
|
-
ctx.ui.notify("No working tree changes found", "warning");
|
|
166
|
-
break;
|
|
167
|
-
case "branch":
|
|
168
|
-
ctx.ui.notify(`No reviewable changes found against ${target.base}`, "warning");
|
|
169
|
-
break;
|
|
170
|
-
case "commit":
|
|
171
|
-
ctx.ui.notify(`Unable to resolve commit ${target.sha}`, "error");
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return undefined;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function notifySynthesisFailure(
|
|
179
|
-
result: Exclude<BriefSynthesisRunResult, { kind: "success" }>,
|
|
180
|
-
ctx: CommandContext,
|
|
181
|
-
): void {
|
|
182
|
-
ctx.ui.notify(
|
|
183
|
-
formatBriefSynthesisFailureCopy(result),
|
|
184
|
-
result.kind === "failed" ? "error" : "warning",
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Persist a brief-synthesis failure so bounded diagnostics remain inspectable in parent history. */
|
|
189
|
-
function injectBriefSynthesisFailureMessage(
|
|
190
|
-
pi: ExtensionAPI,
|
|
191
|
-
result: Exclude<BriefSynthesisRunResult, { kind: "success" }>,
|
|
192
|
-
snapshot: ReviewSnapshot,
|
|
193
|
-
modelId: string,
|
|
194
|
-
): void {
|
|
195
152
|
pi.sendMessage({
|
|
196
153
|
customType: "supi-review",
|
|
197
|
-
content:
|
|
154
|
+
content: pageText(formatReviewBatch(outcome.details)).text,
|
|
198
155
|
display: true,
|
|
199
|
-
details:
|
|
200
|
-
briefSynthesisFailure: { result, snapshot, modelId },
|
|
201
|
-
},
|
|
156
|
+
details: outcome.details,
|
|
202
157
|
});
|
|
203
158
|
}
|
|
204
159
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
snapshot: snapshot.title,
|
|
223
|
-
modelId,
|
|
224
|
-
brief: result.kind === "success" ? result.brief : undefined,
|
|
225
|
-
});
|
|
226
|
-
ringBell();
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Emit a `supi:review:review-done` event and ring the terminal bell after
|
|
231
|
-
* the review child session completes.
|
|
232
|
-
*/
|
|
233
|
-
function notifyReviewDone(pi: ExtensionAPI, result: ReviewResult): void {
|
|
234
|
-
pi.events.emit("supi:review:review-done", {
|
|
235
|
-
kind: result.kind,
|
|
236
|
-
snapshot: result.snapshot.title,
|
|
237
|
-
modelId: result.modelId,
|
|
238
|
-
itemCount: result.kind === "success" ? result.output.items.length : 0,
|
|
160
|
+
export default function reviewExtension(pi: ExtensionAPI): void {
|
|
161
|
+
const planStore = new ReviewPlanStore();
|
|
162
|
+
registerReviewSettings(pi);
|
|
163
|
+
registerAgentReviewTools(pi, planStore);
|
|
164
|
+
|
|
165
|
+
// Message renderer for the /supi-review slash command output.
|
|
166
|
+
// Adapts the sendMessage shape into the shape renderRunResult expects.
|
|
167
|
+
pi.registerMessageRenderer("supi-review", (message, options, theme) => {
|
|
168
|
+
const adapted = {
|
|
169
|
+
content: [{ type: "text" as const, text: message.content as string }],
|
|
170
|
+
details: message.details,
|
|
171
|
+
isError: false,
|
|
172
|
+
};
|
|
173
|
+
const result = renderRunResult(adapted, { ...options, isPartial: false }, theme);
|
|
174
|
+
const box = new Box(options.outputPad, 0, undefined);
|
|
175
|
+
box.addChild(result);
|
|
176
|
+
return box;
|
|
239
177
|
});
|
|
240
|
-
ringBell();
|
|
241
|
-
}
|
|
242
178
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
content: formatReviewContent(result),
|
|
247
|
-
display: true,
|
|
248
|
-
details: { result },
|
|
179
|
+
pi.registerCommand("supi-review", {
|
|
180
|
+
description: "Run one or more caller-defined read-only review tasks",
|
|
181
|
+
handler: async (_args, ctx) => runCommand(ctx, pi, planStore),
|
|
249
182
|
});
|
|
250
|
-
|
|
251
|
-
maybeQueueReviewFollowUp(pi, result);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function maybeQueueReviewFollowUp(pi: ExtensionAPI, result: ReviewResult): void {
|
|
255
|
-
if (result.kind !== "success" || result.output.items.length === 0) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
pi.sendMessage(
|
|
260
|
-
{
|
|
261
|
-
customType: "supi-review-followup",
|
|
262
|
-
content: buildReviewFollowUpInstruction(result),
|
|
263
|
-
display: false,
|
|
264
|
-
details: {
|
|
265
|
-
itemCount: result.output.items.length,
|
|
266
|
-
actionSummary: result.output.summary.actions,
|
|
267
|
-
items: result.output.items.map((item, index) => ({
|
|
268
|
-
number: index + 1,
|
|
269
|
-
title: item.title,
|
|
270
|
-
recommended_action: item.recommended_action,
|
|
271
|
-
})),
|
|
272
|
-
},
|
|
273
|
-
},
|
|
274
|
-
{ triggerTurn: true },
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function buildReviewFollowUpInstruction(
|
|
279
|
-
result: Extract<ReviewResult, { kind: "success" }>,
|
|
280
|
-
): string {
|
|
281
|
-
const { items, summary } = result.output;
|
|
282
|
-
const itemList = items.map(
|
|
283
|
-
(item, index) => `- #${index + 1}: ${item.title} (${item.recommended_action})`,
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
const header =
|
|
287
|
-
"A code review just completed and the result is available in the preceding `supi-review` message.";
|
|
288
|
-
const noFixing = "Do not start fixing code immediately.";
|
|
289
|
-
const useAskUser = "If the `ask_user` tool is available, use it for this decision.";
|
|
290
|
-
|
|
291
|
-
const lines: string[] = [header];
|
|
292
|
-
lines.push(
|
|
293
|
-
`Action summary: ${summary.actions.mustFix} must-fix, ${summary.actions.shouldFix} should-fix, ${summary.actions.consider} consider.`,
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
if (summary.actions.mustFix > 0) {
|
|
297
|
-
lines.push(`${summary.actions.mustFix} must-fix item(s) — fix before merging.`);
|
|
298
|
-
} else if (summary.actions.shouldFix > 0) {
|
|
299
|
-
lines.push(
|
|
300
|
-
`${summary.actions.shouldFix} should-fix item(s) — review carefully before proceeding.`,
|
|
301
|
-
);
|
|
302
|
-
} else if (summary.actions.consider > 0) {
|
|
303
|
-
lines.push(`${summary.actions.consider} consider item(s) — optional follow-ups to review.`);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
lines.push(noFixing, useAskUser);
|
|
307
|
-
lines.push("Offer these options: Fix all, Fix selected, Verify findings, Skip.");
|
|
308
|
-
lines.push(
|
|
309
|
-
"If the user chooses Fix selected, ask a follow-up question listing the review items by number/title.",
|
|
310
|
-
);
|
|
311
|
-
lines.push(
|
|
312
|
-
"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.",
|
|
313
|
-
);
|
|
314
|
-
lines.push("", "Current review items:", ...itemList);
|
|
315
|
-
|
|
316
|
-
return lines.join("\n");
|
|
317
183
|
}
|
|
@@ -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
|
}
|