@mrclrchtr/supi-review 2.8.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.
Files changed (44) hide show
  1. package/README.md +71 -180
  2. package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
  3. package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +40 -0
  5. package/package.json +3 -3
  6. package/src/config.ts +30 -21
  7. package/src/git-command.ts +78 -0
  8. package/src/git.ts +306 -505
  9. package/src/history/collect.ts +43 -120
  10. package/src/model.ts +3 -1
  11. package/src/review-path.ts +85 -0
  12. package/src/review-result.ts +43 -92
  13. package/src/review.ts +154 -290
  14. package/src/session/review-plan-store.ts +20 -51
  15. package/src/target/packet.ts +46 -369
  16. package/src/tool/agent-review-schemas.ts +101 -104
  17. package/src/tool/agent-review-tools.ts +268 -309
  18. package/src/tool/child-failure-diagnostics.ts +1 -1
  19. package/src/tool/child-resource-loader.ts +37 -0
  20. package/src/tool/child-session-runner.ts +83 -0
  21. package/src/tool/output-page.ts +47 -0
  22. package/src/tool/planner-runner.ts +69 -0
  23. package/src/tool/review-runner.ts +42 -257
  24. package/src/tool/review-system-prompt.ts +12 -110
  25. package/src/tool/review-tools.ts +119 -0
  26. package/src/tool/review-workflow.ts +309 -0
  27. package/src/tool/runner-helpers.ts +3 -8
  28. package/src/tool/schemas.ts +75 -62
  29. package/src/tui/common.ts +175 -0
  30. package/src/tui/prepare.ts +132 -0
  31. package/src/tui/run.ts +160 -0
  32. package/src/types.ts +153 -275
  33. package/src/history/synthesize.ts +0 -121
  34. package/src/tool/agent-review-workflow.ts +0 -398
  35. package/src/tool/brief-runner.ts +0 -180
  36. package/src/tool/guidance.ts +0 -21
  37. package/src/tool/review-handlers.ts +0 -314
  38. package/src/tool/snapshot-tools.ts +0 -117
  39. package/src/ui/flow.ts +0 -189
  40. package/src/ui/format-content.ts +0 -143
  41. package/src/ui/renderer.ts +0 -274
  42. package/src/ui/review-plan-inspector.ts +0 -391
  43. package/src/ui/review-tool-format.ts +0 -138
  44. 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 content from a message content value (string | content-part[]). */
9
- export function extractAssistantText(content: unknown): string | undefined {
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
  }
@@ -1,70 +1,83 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import { Type } from "typebox";
3
3
 
4
- const reviewItemCategorySchema = StringEnum([
5
- "correctness",
6
- "security",
7
- "performance",
8
- "api",
9
- "test-gap",
10
- "docs",
11
- "cleanup",
12
- "maintainer",
13
- ] as const);
14
-
15
- const reviewItemImpactSchema = StringEnum(["low", "medium", "high"] as const);
16
-
17
- const reviewItemEffortSchema = StringEnum(["low", "medium", "high"] as const);
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
- export const reviewItemSchema = Type.Object({
33
- title: Type.String(),
34
- body: Type.String(),
35
- category: reviewItemCategorySchema,
36
- impact: reviewItemImpactSchema,
37
- effort: reviewItemEffortSchema,
38
- recommended_action: reviewItemRecommendedActionSchema,
39
- confidence_score: Type.Number({ minimum: 0, maximum: 1 }),
40
- suggested_fix: Type.String(),
41
- verification_hint: Type.String(),
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
- export const reviewOutputSchema = Type.Object({
54
- items: Type.Array(reviewItemSchema),
55
- overall_explanation: Type.String(),
56
- overall_confidence_score: Type.Number({ minimum: 0, maximum: 1 }),
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
- const briefRequiredTextSchema = Type.String({ minLength: 1, maxLength: 4_000 });
60
- const briefListItemSchema = Type.String({ minLength: 1, maxLength: 2_000 });
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
- export const reviewBriefSchema = Type.Object({
63
- summary: briefRequiredTextSchema,
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;