@mrclrchtr/supi-review 2.5.0 → 2.6.1
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 +35 -5
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/package.json +3 -3
- package/src/git.ts +192 -15
- package/src/history/synthesize.ts +4 -5
- package/src/model.ts +19 -0
- package/src/review.ts +2 -2
- package/src/session/review-plan-store.ts +69 -0
- package/src/target/packet.ts +15 -1
- package/src/tool/agent-review-schemas.ts +126 -0
- package/src/tool/agent-review-tools.ts +333 -0
- package/src/tool/agent-review-workflow.ts +380 -0
- package/src/tool/brief-runner.ts +6 -10
- package/src/tool/guidance.ts +21 -0
- package/src/tool/review-debug.ts +2 -0
- package/src/tool/review-handlers.ts +7 -8
- package/src/tool/review-runner.ts +1 -2
- package/src/tool/schemas.ts +34 -38
- package/src/tool/session-lifecycle.ts +7 -3
- package/src/tool/snapshot-tools.ts +2 -1
- package/src/types.ts +104 -3
- package/src/ui/review-tool-format.ts +138 -0
- package/src/ui/review-tool-renderer.ts +203 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import { reviewBriefSchema } from "./schemas.ts";
|
|
4
|
+
|
|
5
|
+
const reviewTargetSchema = Type.Object(
|
|
6
|
+
{
|
|
7
|
+
kind: StringEnum(["working-tree", "branch", "commit"] as const, {
|
|
8
|
+
description: 'Review target kind. Defaults to "working-tree" when target is omitted.',
|
|
9
|
+
}),
|
|
10
|
+
base: Type.Optional(
|
|
11
|
+
Type.String({ description: 'Required local base branch when kind is "branch".' }),
|
|
12
|
+
),
|
|
13
|
+
sha: Type.Optional(Type.String({ description: 'Required commit SHA when kind is "commit".' })),
|
|
14
|
+
},
|
|
15
|
+
{ additionalProperties: false },
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const briefFieldSchema = StringEnum(
|
|
19
|
+
[
|
|
20
|
+
"summary",
|
|
21
|
+
"intendedOutcome",
|
|
22
|
+
"constraints",
|
|
23
|
+
"focusAreas",
|
|
24
|
+
"riskyFiles",
|
|
25
|
+
"unresolvedQuestions",
|
|
26
|
+
// biome-ignore lint/security/noSecrets: public brief field name, not a secret
|
|
27
|
+
"reviewInstructionBlockIds",
|
|
28
|
+
] as const,
|
|
29
|
+
{ description: "Generated brief field being evaluated." },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const critiqueFindingSchema = Type.Object(
|
|
33
|
+
{
|
|
34
|
+
kind: StringEnum(["omission", "unsupported-inference", "misprioritized", "unclear"] as const, {
|
|
35
|
+
description: "Class of brief defect.",
|
|
36
|
+
}),
|
|
37
|
+
field: briefFieldSchema,
|
|
38
|
+
explanation: Type.String({
|
|
39
|
+
minLength: 1,
|
|
40
|
+
maxLength: 2_000,
|
|
41
|
+
description: "What is wrong with this part of the generated brief.",
|
|
42
|
+
}),
|
|
43
|
+
evidence: Type.String({
|
|
44
|
+
minLength: 1,
|
|
45
|
+
maxLength: 2_000,
|
|
46
|
+
description: "Session or snapshot evidence supporting the criticism.",
|
|
47
|
+
}),
|
|
48
|
+
proposedChange: Type.String({
|
|
49
|
+
minLength: 1,
|
|
50
|
+
maxLength: 2_000,
|
|
51
|
+
description: "Concrete change that would improve the brief.",
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
{ additionalProperties: false },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const briefCritiqueSchema = Type.Object(
|
|
58
|
+
{
|
|
59
|
+
verdict: StringEnum(["accept", "revise"] as const, {
|
|
60
|
+
description: 'Use "revise" when the generated brief should be changed before review.',
|
|
61
|
+
}),
|
|
62
|
+
summary: Type.String({
|
|
63
|
+
minLength: 1,
|
|
64
|
+
maxLength: 2_000,
|
|
65
|
+
description: "Concise assessment of the generated brief.",
|
|
66
|
+
}),
|
|
67
|
+
findings: Type.Array(critiqueFindingSchema, {
|
|
68
|
+
maxItems: 20,
|
|
69
|
+
description: "Evidence-backed defects; use an empty array when accepting without criticism.",
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
{ additionalProperties: false },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const reviewerAssignmentSchema = Type.Object(
|
|
76
|
+
{
|
|
77
|
+
id: Type.String({
|
|
78
|
+
minLength: 1,
|
|
79
|
+
maxLength: 64,
|
|
80
|
+
description: 'Stable short label such as "standards" or "spec".',
|
|
81
|
+
}),
|
|
82
|
+
focus: Type.String({
|
|
83
|
+
minLength: 1,
|
|
84
|
+
maxLength: 2_000,
|
|
85
|
+
description: "Independent review focus delegated to this reviewer child session.",
|
|
86
|
+
}),
|
|
87
|
+
},
|
|
88
|
+
{ additionalProperties: false },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
/** Agent-facing schema for preparing a session-aware review plan. */
|
|
92
|
+
export const prepareAgentReviewSchema = Type.Object(
|
|
93
|
+
{
|
|
94
|
+
target: Type.Optional(reviewTargetSchema),
|
|
95
|
+
note: Type.Optional(
|
|
96
|
+
Type.String({
|
|
97
|
+
maxLength: 4_000,
|
|
98
|
+
description: "Optional intent or constraint to emphasize during brief synthesis.",
|
|
99
|
+
}),
|
|
100
|
+
),
|
|
101
|
+
},
|
|
102
|
+
{ additionalProperties: false },
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
/** Agent-facing schema for critiquing a prepared brief and running focused reviewers. */
|
|
106
|
+
export const runAgentReviewSchema = Type.Object(
|
|
107
|
+
{
|
|
108
|
+
planId: Type.String({
|
|
109
|
+
minLength: 1,
|
|
110
|
+
description: "Session-scoped plan id returned by supi_review_prepare.",
|
|
111
|
+
}),
|
|
112
|
+
critique: briefCritiqueSchema,
|
|
113
|
+
revisedBrief: Type.Optional(reviewBriefSchema),
|
|
114
|
+
reviewers: Type.Array(reviewerAssignmentSchema, {
|
|
115
|
+
minItems: 1,
|
|
116
|
+
maxItems: 4,
|
|
117
|
+
description: "One to four independent reviewer assignments run concurrently.",
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
{ additionalProperties: false },
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
/** Validated input accepted by `supi_review_prepare`. */
|
|
124
|
+
export type PrepareAgentReviewInput = Static<typeof prepareAgentReviewSchema>;
|
|
125
|
+
/** Validated input accepted by `supi_review_run`. */
|
|
126
|
+
export type RunAgentReviewInput = Static<typeof runAgentReviewSchema>;
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
buildSessionContext,
|
|
6
|
+
DEFAULT_MAX_BYTES,
|
|
7
|
+
DEFAULT_MAX_LINES,
|
|
8
|
+
type ExtensionAPI,
|
|
9
|
+
formatSize,
|
|
10
|
+
truncateHead,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
|
|
13
|
+
import { isCommitObjectId, summarizeReviewSnapshot } from "../git.ts";
|
|
14
|
+
import { serializeSessionContext } from "../history/collect.ts";
|
|
15
|
+
import { getCurrentReviewModel } from "../model.ts";
|
|
16
|
+
import { ReviewPlanStore } from "../session/review-plan-store.ts";
|
|
17
|
+
import type {
|
|
18
|
+
BriefEvaluation,
|
|
19
|
+
PreparedAgentReviewDetails,
|
|
20
|
+
ReviewProgress,
|
|
21
|
+
ReviewTargetSpec,
|
|
22
|
+
} from "../types.ts";
|
|
23
|
+
import { formatAgentReviewBatch, formatPreparedAgentReview } from "../ui/review-tool-format.ts";
|
|
24
|
+
import {
|
|
25
|
+
type AgentReviewProgressDetails,
|
|
26
|
+
renderPrepareReviewCall,
|
|
27
|
+
renderPrepareReviewResult,
|
|
28
|
+
renderRunReviewCall,
|
|
29
|
+
renderRunReviewResult,
|
|
30
|
+
} from "../ui/review-tool-renderer.ts";
|
|
31
|
+
import {
|
|
32
|
+
type PrepareAgentReviewInput,
|
|
33
|
+
prepareAgentReviewSchema,
|
|
34
|
+
type RunAgentReviewInput,
|
|
35
|
+
runAgentReviewSchema,
|
|
36
|
+
} from "./agent-review-schemas.ts";
|
|
37
|
+
import { prepareAgentReviewPlan, runAgentReviewBatch } from "./agent-review-workflow.ts";
|
|
38
|
+
import {
|
|
39
|
+
PREPARE_REVIEW_TOOL_NAME,
|
|
40
|
+
prepareReviewPromptGuidelines,
|
|
41
|
+
prepareReviewPromptSnippet,
|
|
42
|
+
prepareReviewToolDescription,
|
|
43
|
+
RUN_REVIEW_TOOL_NAME,
|
|
44
|
+
runReviewToolDescription,
|
|
45
|
+
} from "./guidance.ts";
|
|
46
|
+
|
|
47
|
+
const TOOL_OUTPUT_CONTRACT =
|
|
48
|
+
" Output is truncated at 2,000 lines or 50KB; full Markdown is saved to a temporary file when truncated.";
|
|
49
|
+
|
|
50
|
+
/** Register the two-stage agent-driven Session-Aware Review tools. */
|
|
51
|
+
export function registerAgentReviewTools(
|
|
52
|
+
pi: ExtensionAPI,
|
|
53
|
+
planStore = new ReviewPlanStore(),
|
|
54
|
+
): void {
|
|
55
|
+
pi.registerTool({
|
|
56
|
+
name: PREPARE_REVIEW_TOOL_NAME,
|
|
57
|
+
label: "Prepare Review",
|
|
58
|
+
description: prepareReviewToolDescription + TOOL_OUTPUT_CONTRACT,
|
|
59
|
+
promptSnippet: prepareReviewPromptSnippet,
|
|
60
|
+
promptGuidelines: prepareReviewPromptGuidelines,
|
|
61
|
+
parameters: prepareAgentReviewSchema,
|
|
62
|
+
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
63
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
64
|
+
const input = params as PrepareAgentReviewInput;
|
|
65
|
+
const model = getCurrentReviewModel(ctx);
|
|
66
|
+
if (!model) throw new Error("No current session model is available for review preparation.");
|
|
67
|
+
|
|
68
|
+
const target = parseTarget(input);
|
|
69
|
+
const sessionContext = buildSessionContext(
|
|
70
|
+
ctx.sessionManager.getEntries(),
|
|
71
|
+
ctx.sessionManager.getLeafId(),
|
|
72
|
+
);
|
|
73
|
+
const serializedContext = serializeSessionContext(sessionContext.messages);
|
|
74
|
+
const note = input.note?.trim() || undefined;
|
|
75
|
+
|
|
76
|
+
pi.events.emit("supi:working:start", { source: "supi-review" });
|
|
77
|
+
try {
|
|
78
|
+
const outcome = await prepareAgentReviewPlan({
|
|
79
|
+
cwd: ctx.cwd,
|
|
80
|
+
target,
|
|
81
|
+
note,
|
|
82
|
+
serializedContext,
|
|
83
|
+
model,
|
|
84
|
+
signal,
|
|
85
|
+
planStore,
|
|
86
|
+
onProgress: (progress) => {
|
|
87
|
+
onUpdate?.({
|
|
88
|
+
content: [{ type: "text", text: "Synthesizing review brief…" }],
|
|
89
|
+
details: prepareProgressDetails(progress),
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
if (outcome.kind !== "prepared") {
|
|
94
|
+
throw new Error(formatPreparationFailure(outcome));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const details: PreparedAgentReviewDetails = {
|
|
98
|
+
kind: "review-prepared",
|
|
99
|
+
planId: outcome.plan.id,
|
|
100
|
+
briefPromptVersion: outcome.plan.briefPromptVersion,
|
|
101
|
+
generatedBrief: outcome.plan.generatedBrief,
|
|
102
|
+
snapshot: summarizeReviewSnapshot(outcome.plan.snapshot),
|
|
103
|
+
snapshotFingerprint: outcome.plan.snapshotFingerprint,
|
|
104
|
+
modelId: outcome.plan.model.canonicalId,
|
|
105
|
+
};
|
|
106
|
+
activateRunTool(pi);
|
|
107
|
+
return toolResult(formatPreparedAgentReview(details), details, PREPARE_REVIEW_TOOL_NAME);
|
|
108
|
+
} finally {
|
|
109
|
+
pi.events.emit("supi:working:end", { source: "supi-review" });
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
renderCall(args, theme) {
|
|
113
|
+
return renderPrepareReviewCall(args, theme);
|
|
114
|
+
},
|
|
115
|
+
renderResult(result, { expanded }, theme) {
|
|
116
|
+
return renderPrepareReviewResult(result, expanded, theme);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
pi.registerTool({
|
|
121
|
+
name: RUN_REVIEW_TOOL_NAME,
|
|
122
|
+
label: "Run Review",
|
|
123
|
+
description: runReviewToolDescription + TOOL_OUTPUT_CONTRACT,
|
|
124
|
+
parameters: runAgentReviewSchema,
|
|
125
|
+
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
126
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
127
|
+
const input = params as RunAgentReviewInput;
|
|
128
|
+
const progress = createBatchProgress(input.reviewers.map((reviewer) => reviewer.id.trim()));
|
|
129
|
+
|
|
130
|
+
pi.events.emit("supi:working:start", { source: "supi-review" });
|
|
131
|
+
try {
|
|
132
|
+
const outcome = await runAgentReviewBatch({
|
|
133
|
+
cwd: ctx.cwd,
|
|
134
|
+
planId: input.planId,
|
|
135
|
+
critique: input.critique,
|
|
136
|
+
revisedBrief: input.revisedBrief,
|
|
137
|
+
reviewers: input.reviewers,
|
|
138
|
+
signal,
|
|
139
|
+
planStore,
|
|
140
|
+
onBriefEvaluation: (evaluation) => {
|
|
141
|
+
recordBriefCritique(ctx.cwd, evaluation, input.reviewers.length);
|
|
142
|
+
},
|
|
143
|
+
onReviewerProgress: (reviewerId, reviewerProgress) => {
|
|
144
|
+
progress.reviewers[reviewerId] = reviewerProgress;
|
|
145
|
+
onUpdate?.({
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: "text",
|
|
149
|
+
text: `Running reviewers… ${progress.completed}/${progress.total} completed`,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
details: batchProgressDetails(progress),
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
onReviewerDone: (reviewerId) => {
|
|
156
|
+
progress.completed++;
|
|
157
|
+
progress.reviewers[reviewerId] = {
|
|
158
|
+
...(progress.reviewers[reviewerId] ?? { turns: 0, toolUses: 0 }),
|
|
159
|
+
currentFocus: undefined,
|
|
160
|
+
};
|
|
161
|
+
onUpdate?.({
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: "text",
|
|
165
|
+
text: `Running reviewers… ${progress.completed}/${progress.total} completed`,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
details: batchProgressDetails(progress),
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
if (outcome.kind !== "completed") throw new Error(outcome.reason);
|
|
173
|
+
|
|
174
|
+
return toolResult(
|
|
175
|
+
formatAgentReviewBatch(outcome.details),
|
|
176
|
+
outcome.details,
|
|
177
|
+
RUN_REVIEW_TOOL_NAME,
|
|
178
|
+
);
|
|
179
|
+
} finally {
|
|
180
|
+
pi.events.emit("supi:working:end", { source: "supi-review" });
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
renderCall(args, theme, context) {
|
|
184
|
+
return renderRunReviewCall(args, context.expanded, theme);
|
|
185
|
+
},
|
|
186
|
+
renderResult(result, { expanded }, theme) {
|
|
187
|
+
return renderRunReviewResult(result, expanded, theme);
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
pi.on("session_start", () => {
|
|
192
|
+
planStore.clear();
|
|
193
|
+
deactivateRunTool(pi);
|
|
194
|
+
});
|
|
195
|
+
pi.on("session_shutdown", () => {
|
|
196
|
+
planStore.clear();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseTarget(input: PrepareAgentReviewInput): ReviewTargetSpec {
|
|
201
|
+
const target = input.target;
|
|
202
|
+
if (!target) return { kind: "working-tree" };
|
|
203
|
+
if (target.kind === "working-tree") {
|
|
204
|
+
if (target.base || target.sha) {
|
|
205
|
+
throw new Error('target.base and target.sha must be omitted for "working-tree".');
|
|
206
|
+
}
|
|
207
|
+
return { kind: "working-tree" };
|
|
208
|
+
}
|
|
209
|
+
if (target.kind === "branch") {
|
|
210
|
+
if (target.sha) throw new Error('target.sha must be omitted when target.kind is "branch".');
|
|
211
|
+
const base = target.base?.trim();
|
|
212
|
+
if (!base) throw new Error('target.base is required when target.kind is "branch".');
|
|
213
|
+
if (base.startsWith("-")) {
|
|
214
|
+
throw new Error("target.base must name a local branch, not a Git option.");
|
|
215
|
+
}
|
|
216
|
+
return { kind: "branch", base };
|
|
217
|
+
}
|
|
218
|
+
if (target.base) throw new Error('target.base must be omitted when target.kind is "commit".');
|
|
219
|
+
const sha = target.sha?.trim();
|
|
220
|
+
if (!sha) throw new Error('target.sha is required when target.kind is "commit".');
|
|
221
|
+
if (!isCommitObjectId(sha)) {
|
|
222
|
+
throw new Error("target.sha must be a hexadecimal commit object id (7–64 characters).");
|
|
223
|
+
}
|
|
224
|
+
return { kind: "commit", sha };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function formatPreparationFailure(
|
|
228
|
+
outcome: Exclude<Awaited<ReturnType<typeof prepareAgentReviewPlan>>, { kind: "prepared" }>,
|
|
229
|
+
): string {
|
|
230
|
+
if (outcome.kind === "no-snapshot") return outcome.reason;
|
|
231
|
+
switch (outcome.result.kind) {
|
|
232
|
+
case "failed":
|
|
233
|
+
return `Brief synthesis failed: ${outcome.result.reason}`;
|
|
234
|
+
case "canceled":
|
|
235
|
+
return "Brief synthesis was canceled.";
|
|
236
|
+
case "timeout":
|
|
237
|
+
return `Brief synthesis timed out after ${(outcome.result.timeoutMs / 1_000).toFixed(0)}s.`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function activateRunTool(pi: ExtensionAPI): void {
|
|
242
|
+
const active = pi.getActiveTools();
|
|
243
|
+
if (active.includes(RUN_REVIEW_TOOL_NAME)) return;
|
|
244
|
+
pi.setActiveTools([...active, RUN_REVIEW_TOOL_NAME]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function deactivateRunTool(pi: ExtensionAPI): void {
|
|
248
|
+
const active = pi.getActiveTools();
|
|
249
|
+
if (!active.includes(RUN_REVIEW_TOOL_NAME)) return;
|
|
250
|
+
pi.setActiveTools(active.filter((name) => name !== RUN_REVIEW_TOOL_NAME));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function prepareProgressDetails(progress: ReviewProgress): AgentReviewProgressDetails {
|
|
254
|
+
return {
|
|
255
|
+
kind: "review-progress",
|
|
256
|
+
phase: "prepare",
|
|
257
|
+
completed: 0,
|
|
258
|
+
total: 1,
|
|
259
|
+
reviewers: { synthesis: progress },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface BatchProgressState {
|
|
264
|
+
completed: number;
|
|
265
|
+
total: number;
|
|
266
|
+
reviewers: Record<string, ReviewProgress>;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function createBatchProgress(reviewerIds: string[]): BatchProgressState {
|
|
270
|
+
return {
|
|
271
|
+
completed: 0,
|
|
272
|
+
total: reviewerIds.length,
|
|
273
|
+
reviewers: Object.fromEntries(
|
|
274
|
+
reviewerIds.map((id) => [id, { turns: 0, toolUses: 0 } satisfies ReviewProgress]),
|
|
275
|
+
),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function batchProgressDetails(progress: BatchProgressState): AgentReviewProgressDetails {
|
|
280
|
+
return {
|
|
281
|
+
kind: "review-progress",
|
|
282
|
+
phase: "review",
|
|
283
|
+
completed: progress.completed,
|
|
284
|
+
total: progress.total,
|
|
285
|
+
reviewers: { ...progress.reviewers },
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function recordBriefCritique(
|
|
290
|
+
cwd: string,
|
|
291
|
+
evaluation: BriefEvaluation,
|
|
292
|
+
reviewerCount: number,
|
|
293
|
+
): void {
|
|
294
|
+
recordDebugEvent({
|
|
295
|
+
source: "supi-review",
|
|
296
|
+
level: "info",
|
|
297
|
+
category: "brief-critique",
|
|
298
|
+
message: `Main agent submitted a brief critique with verdict ${evaluation.critique.verdict}`,
|
|
299
|
+
cwd,
|
|
300
|
+
data: {
|
|
301
|
+
planId: evaluation.planId,
|
|
302
|
+
briefPromptVersion: evaluation.briefPromptVersion,
|
|
303
|
+
verdict: evaluation.critique.verdict,
|
|
304
|
+
findingCount: evaluation.critique.findings.length,
|
|
305
|
+
reviewerCount,
|
|
306
|
+
modelId: evaluation.synthesizerModelId,
|
|
307
|
+
snapshotFingerprint: evaluation.snapshotFingerprint,
|
|
308
|
+
},
|
|
309
|
+
rawData: evaluation,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function toolResult<TDetails>(content: string, details: TDetails, toolName: string) {
|
|
314
|
+
const truncation = truncateHead(content, {
|
|
315
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
316
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
317
|
+
});
|
|
318
|
+
if (!truncation.truncated) {
|
|
319
|
+
return { content: [{ type: "text" as const, text: truncation.content }], details };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const dir = mkdtempSync(join(tmpdir(), "supi-review-"));
|
|
323
|
+
const file = join(dir, `${toolName}.md`);
|
|
324
|
+
writeFileSync(file, content, { encoding: "utf-8", mode: 0o600 });
|
|
325
|
+
const note =
|
|
326
|
+
`\n\n[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines ` +
|
|
327
|
+
`(${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). ` +
|
|
328
|
+
`Full output saved to: ${file}]`;
|
|
329
|
+
return {
|
|
330
|
+
content: [{ type: "text" as const, text: truncation.content + note }],
|
|
331
|
+
details,
|
|
332
|
+
};
|
|
333
|
+
}
|