@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { listReviewFiles, readReviewDiff, readReviewFile, searchReviewFiles } from "../git.ts";
|
|
5
|
+
import { normalizeReviewSubmission } from "../review-result.ts";
|
|
6
|
+
import type { ReviewSnapshot, ReviewSubmission } from "../types.ts";
|
|
7
|
+
import { DEFAULT_PAGE_CHARACTERS, MAX_PAGE_CHARACTERS, pageText } from "./output-page.ts";
|
|
8
|
+
import { reviewSubmissionSchema } from "./schemas.ts";
|
|
9
|
+
|
|
10
|
+
const paginationProperties = {
|
|
11
|
+
offset: Type.Optional(
|
|
12
|
+
Type.Integer({ minimum: 0, default: 0, description: "UTF-16 character offset." }),
|
|
13
|
+
),
|
|
14
|
+
limit: Type.Optional(
|
|
15
|
+
Type.Integer({
|
|
16
|
+
minimum: 1,
|
|
17
|
+
maximum: MAX_PAGE_CHARACTERS,
|
|
18
|
+
default: DEFAULT_PAGE_CHARACTERS,
|
|
19
|
+
description: "Maximum characters for this page.",
|
|
20
|
+
}),
|
|
21
|
+
),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function pagedResult(text: string, offset?: number, limit?: number) {
|
|
25
|
+
const page = pageText(text, offset, limit);
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text" as const, text: page.text }],
|
|
28
|
+
details: {
|
|
29
|
+
offset: page.offset,
|
|
30
|
+
nextOffset: page.nextOffset,
|
|
31
|
+
totalCharacters: page.totalCharacters,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Build the complete fixed tool set for one resolved review target. */
|
|
37
|
+
export function createReviewTools(
|
|
38
|
+
cwd: string,
|
|
39
|
+
snapshot: ReviewSnapshot,
|
|
40
|
+
submission: { value?: ReviewSubmission },
|
|
41
|
+
) {
|
|
42
|
+
return [
|
|
43
|
+
defineTool({
|
|
44
|
+
name: "list_review_files",
|
|
45
|
+
label: "List Review Files",
|
|
46
|
+
description: "List after-side target files. Use offset to continue paged output.",
|
|
47
|
+
parameters: Type.Object(paginationProperties, { additionalProperties: false }),
|
|
48
|
+
execute: async (_id, args) =>
|
|
49
|
+
pagedResult((await listReviewFiles(cwd, snapshot)).join("\n"), args.offset, args.limit),
|
|
50
|
+
}),
|
|
51
|
+
defineTool({
|
|
52
|
+
name: "read_review_diff",
|
|
53
|
+
label: "Read Review Diff",
|
|
54
|
+
description: "Read one changed file's exact target diff. Use offset to continue.",
|
|
55
|
+
parameters: Type.Object(
|
|
56
|
+
{ path: Type.String({ minLength: 1 }), ...paginationProperties },
|
|
57
|
+
{ additionalProperties: false },
|
|
58
|
+
),
|
|
59
|
+
execute: async (_id, args) =>
|
|
60
|
+
pagedResult(await readReviewDiff(cwd, snapshot, args.path), args.offset, args.limit),
|
|
61
|
+
}),
|
|
62
|
+
defineTool({
|
|
63
|
+
name: "read_review_file",
|
|
64
|
+
label: "Read Review File",
|
|
65
|
+
description: "Read a before/after target file. Use offset to continue paged output.",
|
|
66
|
+
parameters: Type.Object(
|
|
67
|
+
{
|
|
68
|
+
path: Type.String({ minLength: 1 }),
|
|
69
|
+
side: Type.Optional(
|
|
70
|
+
StringEnum(["before", "after"] as const, {
|
|
71
|
+
default: "after",
|
|
72
|
+
description: "Target side to read; defaults to after.",
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
75
|
+
...paginationProperties,
|
|
76
|
+
},
|
|
77
|
+
{ additionalProperties: false },
|
|
78
|
+
),
|
|
79
|
+
execute: async (_id, args) => {
|
|
80
|
+
const content = await readReviewFile(cwd, snapshot, args.path, args.side ?? "after");
|
|
81
|
+
return pagedResult(content ?? "[file unavailable on this side]", args.offset, args.limit);
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
defineTool({
|
|
85
|
+
name: "search_review_files",
|
|
86
|
+
label: "Search Review Files",
|
|
87
|
+
description: "Search literal after-side target text. Use offset to continue paged output.",
|
|
88
|
+
parameters: Type.Object(
|
|
89
|
+
{
|
|
90
|
+
query: Type.String({ minLength: 1 }),
|
|
91
|
+
path: Type.Optional(Type.String({ minLength: 1 })),
|
|
92
|
+
...paginationProperties,
|
|
93
|
+
},
|
|
94
|
+
{ additionalProperties: false },
|
|
95
|
+
),
|
|
96
|
+
execute: async (_id, args) =>
|
|
97
|
+
pagedResult(
|
|
98
|
+
(await searchReviewFiles(cwd, snapshot, args.query, args.path)) || "No matches.",
|
|
99
|
+
args.offset,
|
|
100
|
+
args.limit,
|
|
101
|
+
),
|
|
102
|
+
}),
|
|
103
|
+
defineTool({
|
|
104
|
+
name: "submit_review",
|
|
105
|
+
label: "Submit Review",
|
|
106
|
+
description: "Submit the final structured result for this review task.",
|
|
107
|
+
parameters: reviewSubmissionSchema,
|
|
108
|
+
execute: async (_id, args) => {
|
|
109
|
+
const { verdict: _, ...normalized } = normalizeReviewSubmission(args as ReviewSubmission);
|
|
110
|
+
submission.value = normalized;
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: "text" as const, text: "Review submitted." }],
|
|
113
|
+
details: normalized,
|
|
114
|
+
terminate: true,
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
];
|
|
119
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { resolveReviewSnapshot, summarizeReviewSnapshot } from "../git.ts";
|
|
2
|
+
import { normalizeReviewSubmission } from "../review-result.ts";
|
|
3
|
+
import type { ReviewPlanStore } from "../session/review-plan-store.ts";
|
|
4
|
+
import { buildReviewPacket } from "../target/packet.ts";
|
|
5
|
+
import type {
|
|
6
|
+
PlannerDraft,
|
|
7
|
+
PlanningRecord,
|
|
8
|
+
ReviewBatchDetails,
|
|
9
|
+
ReviewInput,
|
|
10
|
+
ReviewModelSelection,
|
|
11
|
+
ReviewSnapshot,
|
|
12
|
+
ReviewTargetSpec,
|
|
13
|
+
ReviewTaskResult,
|
|
14
|
+
} from "../types.ts";
|
|
15
|
+
import { createUnobservedChildFailureDiagnostics } from "./child-failure-diagnostics.ts";
|
|
16
|
+
import { PLANNER_PROMPT_VERSION, runPlanner } from "./planner-runner.ts";
|
|
17
|
+
import { runReviewer } from "./review-runner.ts";
|
|
18
|
+
|
|
19
|
+
/** Input required by the prepare workflow: target resolution plus optional Planner run. */
|
|
20
|
+
export interface PrepareReviewInput {
|
|
21
|
+
cwd: string;
|
|
22
|
+
target: ReviewTargetSpec;
|
|
23
|
+
planning: "none" | "suggest";
|
|
24
|
+
plannerContext: string;
|
|
25
|
+
reviewerModel: ReviewModelSelection;
|
|
26
|
+
plannerModel?: ReviewModelSelection;
|
|
27
|
+
planStore: ReviewPlanStore;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
onUpdate?: OnUpdate;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Tool update callback signature shared across workflow adapters. */
|
|
33
|
+
type OnUpdate = (result: {
|
|
34
|
+
content: Array<{ type: "text"; text: string }>;
|
|
35
|
+
details: Record<string, unknown>;
|
|
36
|
+
}) => void;
|
|
37
|
+
|
|
38
|
+
/** Complete Direct Review request: target, full review input, and reviewer model. */
|
|
39
|
+
export interface DirectRunInput {
|
|
40
|
+
mode: "direct";
|
|
41
|
+
cwd: string;
|
|
42
|
+
target: ReviewTargetSpec;
|
|
43
|
+
review: ReviewInput;
|
|
44
|
+
reviewerModel: ReviewModelSelection;
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
onUpdate?: OnUpdate;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One-shot Prepared Review request: plan id, explicit decision, and plan store. */
|
|
50
|
+
export interface PreparedRunInput {
|
|
51
|
+
mode: "prepared";
|
|
52
|
+
cwd: string;
|
|
53
|
+
planId: string;
|
|
54
|
+
decision: { kind: "accept-draft" } | { kind: "use-review"; review: ReviewInput };
|
|
55
|
+
planStore: ReviewPlanStore;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
onUpdate?: OnUpdate;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Discriminated union accepted by `runReview` for both Direct and Prepared paths. */
|
|
61
|
+
export type RunReviewInput = DirectRunInput | PreparedRunInput;
|
|
62
|
+
|
|
63
|
+
const MAX_PLANNER_FILE_COUNT = 200;
|
|
64
|
+
const MAX_PLANNER_FILE_CHARS = 8_000;
|
|
65
|
+
|
|
66
|
+
/** Canonicalize and semantically validate review input for every adapter. */
|
|
67
|
+
export function normalizeReviewInput(review: ReviewInput): ReviewInput {
|
|
68
|
+
const sharedContext = review.sharedContext?.trim();
|
|
69
|
+
const tasks = review.tasks.map((task) => ({
|
|
70
|
+
id: task.id.trim(),
|
|
71
|
+
instructions: task.instructions.trim(),
|
|
72
|
+
}));
|
|
73
|
+
if (tasks.length < 1 || tasks.length > 4)
|
|
74
|
+
throw new Error("Provide between one and four review tasks.");
|
|
75
|
+
if (tasks.some((task) => !task.id || !task.instructions)) {
|
|
76
|
+
throw new Error("Review task ids and instructions must not be blank.");
|
|
77
|
+
}
|
|
78
|
+
if (tasks.some((task) => task.id.length > 64)) {
|
|
79
|
+
throw new Error("Review task ids must not exceed 64 characters.");
|
|
80
|
+
}
|
|
81
|
+
if (new Set(tasks.map((task) => task.id)).size !== tasks.length) {
|
|
82
|
+
throw new Error("Review task ids must be unique.");
|
|
83
|
+
}
|
|
84
|
+
return { ...(sharedContext ? { sharedContext } : {}), tasks };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function plannerFileManifest(files: string[]): string[] {
|
|
88
|
+
const lines: string[] = [];
|
|
89
|
+
let size = 0;
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
const line = `- ${JSON.stringify(file)}`;
|
|
92
|
+
if (
|
|
93
|
+
lines.length >= MAX_PLANNER_FILE_COUNT ||
|
|
94
|
+
size + line.length + 1 > MAX_PLANNER_FILE_CHARS - 100
|
|
95
|
+
) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
lines.push(line);
|
|
99
|
+
size += line.length + 1;
|
|
100
|
+
}
|
|
101
|
+
const omitted = files.length - lines.length;
|
|
102
|
+
if (omitted > 0) lines.push(`- … ${omitted} additional file(s) omitted`);
|
|
103
|
+
return lines;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function plannerPrompt(snapshot: ReviewSnapshot, context: string): string {
|
|
107
|
+
return [
|
|
108
|
+
"# Review Planning Input",
|
|
109
|
+
"",
|
|
110
|
+
`Target: ${snapshot.title}`,
|
|
111
|
+
`Changed files: ${snapshot.changedFiles.length}`,
|
|
112
|
+
`Diff stats: +${snapshot.stats.additions} / -${snapshot.stats.deletions}`,
|
|
113
|
+
"",
|
|
114
|
+
"## Changed file names",
|
|
115
|
+
...plannerFileManifest(snapshot.changedFiles),
|
|
116
|
+
"",
|
|
117
|
+
"## Bounded session conversation",
|
|
118
|
+
context || "No session conversation was available.",
|
|
119
|
+
].join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Resolve and store a one-shot plan, optionally asking the advisory Planner for tasks. */
|
|
123
|
+
export async function prepareReview(input: PrepareReviewInput) {
|
|
124
|
+
input.onUpdate?.({
|
|
125
|
+
content: [{ type: "text", text: "Resolving target…" }],
|
|
126
|
+
details: {},
|
|
127
|
+
});
|
|
128
|
+
const snapshot = await resolveReviewSnapshot(input.cwd, input.target);
|
|
129
|
+
if (!snapshot) return { kind: "no-target" as const, reason: "No reviewable changes found." };
|
|
130
|
+
let plannerDraft: PlannerDraft | undefined;
|
|
131
|
+
if (input.planning === "suggest") {
|
|
132
|
+
if (!input.plannerModel) throw new Error("A configured Planner model is required.");
|
|
133
|
+
input.onUpdate?.({
|
|
134
|
+
content: [{ type: "text", text: "Running planner…" }],
|
|
135
|
+
details: {},
|
|
136
|
+
});
|
|
137
|
+
const result = await runPlanner({
|
|
138
|
+
cwd: input.cwd,
|
|
139
|
+
model: input.plannerModel.model,
|
|
140
|
+
prompt: plannerPrompt(snapshot, input.plannerContext),
|
|
141
|
+
signal: input.signal,
|
|
142
|
+
});
|
|
143
|
+
if (result.kind !== "success") return { kind: "planner-failed" as const, result };
|
|
144
|
+
plannerDraft = normalizeReviewInput(result.draft);
|
|
145
|
+
}
|
|
146
|
+
const plan = input.planStore.create({
|
|
147
|
+
snapshot,
|
|
148
|
+
reviewerModel: input.reviewerModel,
|
|
149
|
+
...(plannerDraft ? { plannerDraft } : {}),
|
|
150
|
+
...(input.plannerModel ? { plannerModelId: input.plannerModel.canonicalId } : {}),
|
|
151
|
+
...(plannerDraft ? { plannerPromptVersion: PLANNER_PROMPT_VERSION } : {}),
|
|
152
|
+
});
|
|
153
|
+
return { kind: "prepared" as const, plan };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function toTaskResult(
|
|
157
|
+
taskId: string,
|
|
158
|
+
packetHash: string,
|
|
159
|
+
result: Awaited<ReturnType<typeof runReviewer>>,
|
|
160
|
+
): ReviewTaskResult {
|
|
161
|
+
const identity = { taskId, packetHash, modelId: result.modelId };
|
|
162
|
+
if (result.kind === "success") {
|
|
163
|
+
const normalized = normalizeReviewSubmission(result.submission);
|
|
164
|
+
return {
|
|
165
|
+
status: "completed",
|
|
166
|
+
...identity,
|
|
167
|
+
verdict: normalized.verdict,
|
|
168
|
+
summary: normalized.summary,
|
|
169
|
+
findings: normalized.findings,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (result.kind === "failed") {
|
|
173
|
+
return {
|
|
174
|
+
status: "failed",
|
|
175
|
+
...identity,
|
|
176
|
+
failureCode: result.failureCode,
|
|
177
|
+
...(result.diagnostics ? { diagnostics: result.diagnostics } : {}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (result.kind === "canceled") {
|
|
181
|
+
return { status: "canceled", ...identity, diagnostics: result.diagnostics };
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
status: "timeout",
|
|
185
|
+
...identity,
|
|
186
|
+
timeoutMs: result.timeoutMs,
|
|
187
|
+
diagnostics: result.diagnostics,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// biome-ignore lint/complexity/useMaxParams: compact internal fan-out helper
|
|
192
|
+
async function execute(
|
|
193
|
+
cwd: string,
|
|
194
|
+
snapshot: ReviewSnapshot,
|
|
195
|
+
reviewInput: ReviewInput,
|
|
196
|
+
model: ReviewModelSelection,
|
|
197
|
+
signal?: AbortSignal,
|
|
198
|
+
onUpdate?: OnUpdate,
|
|
199
|
+
): Promise<ReviewTaskResult[]> {
|
|
200
|
+
const review = normalizeReviewInput(reviewInput);
|
|
201
|
+
let completedCount = 0;
|
|
202
|
+
const totalCount = review.tasks.length;
|
|
203
|
+
|
|
204
|
+
return Promise.all(
|
|
205
|
+
review.tasks.map(async (task) => {
|
|
206
|
+
const packet = buildReviewPacket(snapshot, review, task, model);
|
|
207
|
+
try {
|
|
208
|
+
const result = await runReviewer({
|
|
209
|
+
cwd,
|
|
210
|
+
snapshot,
|
|
211
|
+
task,
|
|
212
|
+
prompt: packet.prompt,
|
|
213
|
+
model,
|
|
214
|
+
signal,
|
|
215
|
+
});
|
|
216
|
+
const taskResult = toTaskResult(task.id, packet.packetHash, result);
|
|
217
|
+
completedCount++;
|
|
218
|
+
const verb = taskResult.status === "completed" ? "complete" : taskResult.status;
|
|
219
|
+
onUpdate?.({
|
|
220
|
+
content: [
|
|
221
|
+
{ type: "text", text: `Task ${task.id} ${verb} (${completedCount} of ${totalCount})` },
|
|
222
|
+
],
|
|
223
|
+
details: { completedCount, totalCount },
|
|
224
|
+
});
|
|
225
|
+
return taskResult;
|
|
226
|
+
} catch {
|
|
227
|
+
const taskResult: ReviewTaskResult = {
|
|
228
|
+
status: "failed",
|
|
229
|
+
taskId: task.id,
|
|
230
|
+
packetHash: packet.packetHash,
|
|
231
|
+
modelId: model.canonicalId,
|
|
232
|
+
failureCode: "unexpected-runner-failure",
|
|
233
|
+
diagnostics: createUnobservedChildFailureDiagnostics(),
|
|
234
|
+
};
|
|
235
|
+
completedCount++;
|
|
236
|
+
onUpdate?.({
|
|
237
|
+
content: [
|
|
238
|
+
{
|
|
239
|
+
type: "text",
|
|
240
|
+
text: `Task ${task.id} failed (${completedCount} of ${totalCount})`,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
details: { completedCount, totalCount },
|
|
244
|
+
});
|
|
245
|
+
return taskResult;
|
|
246
|
+
}
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Execute a Direct Review or atomically consume and execute a Prepared Review. */
|
|
252
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: direct/prepared orchestration stays at one public seam
|
|
253
|
+
export async function runReview(input: RunReviewInput) {
|
|
254
|
+
let snapshot: ReviewSnapshot;
|
|
255
|
+
let review: ReviewInput;
|
|
256
|
+
let model: ReviewModelSelection;
|
|
257
|
+
let planning: PlanningRecord | undefined;
|
|
258
|
+
let provenance: ReviewBatchDetails["provenance"] = "caller-supplied";
|
|
259
|
+
|
|
260
|
+
if (input.mode === "direct") {
|
|
261
|
+
const resolved = await resolveReviewSnapshot(input.cwd, input.target);
|
|
262
|
+
if (!resolved) return { kind: "no-target" as const, reason: "No reviewable changes found." };
|
|
263
|
+
snapshot = resolved;
|
|
264
|
+
review = normalizeReviewInput(input.review);
|
|
265
|
+
model = input.reviewerModel;
|
|
266
|
+
} else {
|
|
267
|
+
const plan = input.planStore.take(input.planId);
|
|
268
|
+
if (!plan) {
|
|
269
|
+
return {
|
|
270
|
+
kind: "invalid" as const,
|
|
271
|
+
reason: `Review Plan ${input.planId} was not found or was already consumed.`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (input.decision.kind === "accept-draft" && !plan.plannerDraft) {
|
|
275
|
+
return { kind: "invalid" as const, reason: "This plan has no Planner Draft." };
|
|
276
|
+
}
|
|
277
|
+
const selectedReview =
|
|
278
|
+
input.decision.kind === "accept-draft"
|
|
279
|
+
? plan.plannerDraft
|
|
280
|
+
: normalizeReviewInput(input.decision.review);
|
|
281
|
+
if (!selectedReview) {
|
|
282
|
+
return { kind: "invalid" as const, reason: "This plan has no Planner Draft." };
|
|
283
|
+
}
|
|
284
|
+
snapshot = plan.snapshot;
|
|
285
|
+
review = selectedReview;
|
|
286
|
+
model = plan.reviewerModel;
|
|
287
|
+
if (plan.plannerDraft) provenance = "planner-assisted";
|
|
288
|
+
if (plan.plannerDraft && plan.plannerModelId && plan.plannerPromptVersion) {
|
|
289
|
+
planning = {
|
|
290
|
+
promptVersion: plan.plannerPromptVersion,
|
|
291
|
+
modelId: plan.plannerModelId,
|
|
292
|
+
draft: plan.plannerDraft,
|
|
293
|
+
effectiveReview: review,
|
|
294
|
+
decision: input.decision.kind,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const details: ReviewBatchDetails = {
|
|
300
|
+
kind: "review-batch",
|
|
301
|
+
mode: input.mode,
|
|
302
|
+
provenance,
|
|
303
|
+
snapshot: summarizeReviewSnapshot(snapshot),
|
|
304
|
+
review,
|
|
305
|
+
...(planning ? { planning } : {}),
|
|
306
|
+
results: await execute(input.cwd, snapshot, review, model, input.signal, input.onUpdate),
|
|
307
|
+
};
|
|
308
|
+
return { kind: "completed" as const, details };
|
|
309
|
+
}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared helpers for child-session runners (brief synthesis & review).
|
|
3
|
-
*
|
|
4
|
-
* These were extracted from brief-runner.ts and review-runner.ts to
|
|
5
|
-
* eliminate duplication.
|
|
6
|
-
*/
|
|
1
|
+
/** Shared child-session helpers that do not retain model-generated diagnostics. */
|
|
7
2
|
|
|
8
|
-
/** Extract text
|
|
9
|
-
export function
|
|
3
|
+
/** Extract visible text from a message content value (string or content-part array). */
|
|
4
|
+
export function extractVisibleText(content: unknown): string | undefined {
|
|
10
5
|
if (typeof content === "string") {
|
|
11
6
|
return content || undefined;
|
|
12
7
|
}
|
package/src/tool/schemas.ts
CHANGED
|
@@ -1,70 +1,83 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const reviewItemRecommendedActionSchema = StringEnum([
|
|
20
|
-
"must-fix",
|
|
21
|
-
"should-fix",
|
|
22
|
-
"consider",
|
|
23
|
-
] as const);
|
|
24
|
-
|
|
25
|
-
const reviewInstructionBlockIdSchema = StringEnum([
|
|
26
|
-
"public-surface",
|
|
27
|
-
"cross-layer",
|
|
28
|
-
"schema-widening",
|
|
29
|
-
"cleanup",
|
|
30
|
-
] as const);
|
|
4
|
+
/** TypeBox schema for one caller-defined review task (provider-visible). */
|
|
5
|
+
export const reviewTaskSchema = Type.Object(
|
|
6
|
+
{
|
|
7
|
+
id: Type.String({
|
|
8
|
+
minLength: 1,
|
|
9
|
+
maxLength: 64,
|
|
10
|
+
description: "Stable caller-defined task id, such as standards or spec.",
|
|
11
|
+
}),
|
|
12
|
+
instructions: Type.String({
|
|
13
|
+
minLength: 1,
|
|
14
|
+
description: "Complete freeform methodology and acceptance instructions for this task.",
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
{ additionalProperties: false },
|
|
18
|
+
);
|
|
31
19
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
code_location: Type.Optional(
|
|
43
|
-
Type.Object({
|
|
44
|
-
absolute_file_path: Type.String(),
|
|
45
|
-
line_range: Type.Object({
|
|
46
|
-
start: Type.Number(),
|
|
47
|
-
end: Type.Number(),
|
|
48
|
-
}),
|
|
20
|
+
/** TypeBox schema for the complete review input (provider-visible). */
|
|
21
|
+
export const reviewInputSchema = Type.Object(
|
|
22
|
+
{
|
|
23
|
+
sharedContext: Type.Optional(
|
|
24
|
+
Type.String({ description: "Optional context shared unchanged with every task." }),
|
|
25
|
+
),
|
|
26
|
+
tasks: Type.Array(reviewTaskSchema, {
|
|
27
|
+
minItems: 1,
|
|
28
|
+
maxItems: 4,
|
|
29
|
+
description: "One to four independent tasks executed concurrently in caller order.",
|
|
49
30
|
}),
|
|
50
|
-
|
|
51
|
-
}
|
|
31
|
+
},
|
|
32
|
+
{ additionalProperties: false },
|
|
33
|
+
);
|
|
52
34
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
35
|
+
/** TypeBox schema for a single structured finding (provider-visible). */
|
|
36
|
+
export const reviewFindingSchema = Type.Object(
|
|
37
|
+
{
|
|
38
|
+
title: Type.String({ minLength: 1, description: "Concise finding title." }),
|
|
39
|
+
description: Type.String({
|
|
40
|
+
minLength: 1,
|
|
41
|
+
description: "Concrete evidence-backed explanation of the issue.",
|
|
42
|
+
}),
|
|
43
|
+
blocksAcceptance: Type.Boolean({
|
|
44
|
+
description: "Whether this finding must be corrected before accepting the change.",
|
|
45
|
+
}),
|
|
46
|
+
impact: StringEnum(["low", "medium", "high"] as const, {
|
|
47
|
+
description: "Downside if the issue remains unfixed.",
|
|
48
|
+
}),
|
|
49
|
+
effort: StringEnum(["small", "medium", "large"] as const, {
|
|
50
|
+
description: "Estimated size of the correction.",
|
|
51
|
+
}),
|
|
52
|
+
confidence: Type.Number({
|
|
53
|
+
minimum: 0,
|
|
54
|
+
maximum: 1,
|
|
55
|
+
description: "Confidence from 0 through 1; findings are never threshold-filtered.",
|
|
56
|
+
}),
|
|
57
|
+
location: Type.Optional(
|
|
58
|
+
Type.Object(
|
|
59
|
+
{
|
|
60
|
+
path: Type.String({ minLength: 1, description: "Repository-relative target path." }),
|
|
61
|
+
startLine: Type.Integer({ minimum: 1 }),
|
|
62
|
+
endLine: Type.Integer({ minimum: 1 }),
|
|
63
|
+
},
|
|
64
|
+
{ additionalProperties: false },
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
},
|
|
68
|
+
{ additionalProperties: false },
|
|
69
|
+
);
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
const
|
|
71
|
+
/** TypeBox schema for the submit_review tool parameters (provider-visible). */
|
|
72
|
+
export const reviewSubmissionSchema = Type.Object(
|
|
73
|
+
{
|
|
74
|
+
summary: Type.String({ minLength: 1, description: "Task-level review summary." }),
|
|
75
|
+
findings: Type.Array(reviewFindingSchema, {
|
|
76
|
+
description: "Findings in the reviewer's intended order; use an empty array when none exist.",
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
{ additionalProperties: false },
|
|
80
|
+
);
|
|
61
81
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
intendedOutcome: briefRequiredTextSchema,
|
|
65
|
-
constraints: Type.Array(briefListItemSchema, { maxItems: 50 }),
|
|
66
|
-
focusAreas: Type.Array(briefListItemSchema, { maxItems: 50 }),
|
|
67
|
-
riskyFiles: Type.Array(briefListItemSchema, { maxItems: 200 }),
|
|
68
|
-
unresolvedQuestions: Type.Array(briefListItemSchema, { maxItems: 50 }),
|
|
69
|
-
reviewInstructionBlockIds: Type.Array(reviewInstructionBlockIdSchema, { maxItems: 4 }),
|
|
70
|
-
});
|
|
82
|
+
/** Planner Draft uses the same shape as review input (shared context + tasks). */
|
|
83
|
+
export const plannerDraftSchema = reviewInputSchema;
|