@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.
Files changed (49) hide show
  1. package/README.md +75 -163
  2. package/node_modules/@mrclrchtr/supi-core/README.md +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
  4. package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +40 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +1 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +14 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +31 -14
  9. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
  10. package/package.json +3 -3
  11. package/src/config.ts +64 -0
  12. package/src/git-command.ts +78 -0
  13. package/src/git.ts +306 -505
  14. package/src/history/collect.ts +43 -120
  15. package/src/model.ts +35 -0
  16. package/src/review-path.ts +85 -0
  17. package/src/review-result.ts +43 -92
  18. package/src/review.ts +154 -288
  19. package/src/session/review-plan-store.ts +20 -51
  20. package/src/target/packet.ts +46 -369
  21. package/src/tool/agent-review-schemas.ts +101 -104
  22. package/src/tool/agent-review-tools.ts +269 -301
  23. package/src/tool/child-failure-diagnostics.ts +1 -1
  24. package/src/tool/child-resource-loader.ts +37 -0
  25. package/src/tool/child-session-runner.ts +83 -0
  26. package/src/tool/output-page.ts +47 -0
  27. package/src/tool/planner-runner.ts +69 -0
  28. package/src/tool/review-runner.ts +42 -257
  29. package/src/tool/review-system-prompt.ts +12 -110
  30. package/src/tool/review-tools.ts +119 -0
  31. package/src/tool/review-workflow.ts +309 -0
  32. package/src/tool/runner-helpers.ts +3 -8
  33. package/src/tool/schemas.ts +75 -62
  34. package/src/tui/common.ts +175 -0
  35. package/src/tui/prepare.ts +132 -0
  36. package/src/tui/run.ts +160 -0
  37. package/src/types.ts +153 -275
  38. package/src/history/synthesize.ts +0 -121
  39. package/src/tool/agent-review-workflow.ts +0 -398
  40. package/src/tool/brief-runner.ts +0 -180
  41. package/src/tool/guidance.ts +0 -21
  42. package/src/tool/review-handlers.ts +0 -314
  43. package/src/tool/snapshot-tools.ts +0 -117
  44. package/src/ui/flow.ts +0 -189
  45. package/src/ui/format-content.ts +0 -143
  46. package/src/ui/renderer.ts +0 -274
  47. package/src/ui/review-plan-inspector.ts +0 -391
  48. package/src/ui/review-tool-format.ts +0 -138
  49. package/src/ui/review-tool-renderer.ts +0 -234
@@ -1,333 +1,301 @@
1
- import { mkdtempSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
1
+ import { buildSessionContext, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { StatusSpinner } from "@mrclrchtr/supi-core/status-spinner";
3
+ import { loadReviewConfig } from "../config.ts";
4
+ import { summarizeReviewSnapshot } from "../git.ts";
5
+ import { collectPlannerContext } from "../history/collect.ts";
6
+ import { resolveAgentReviewModel } from "../model.ts";
7
+ import type { ReviewPlanStore } from "../session/review-plan-store.ts";
8
+ import { renderPrepareCall, renderPrepareResult } from "../tui/prepare.ts";
9
+ import { renderRunCall, renderRunResult } from "../tui/run.ts";
10
+ import type { ReviewBatchDetails, ReviewTargetSpec, ReviewTaskResult } from "../types.ts";
4
11
  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 { formatBriefSynthesisFailureCopy } from "../ui/format-content.ts";
24
- import { formatAgentReviewBatch, formatPreparedAgentReview } from "../ui/review-tool-format.ts";
25
- import {
26
- type AgentReviewProgressDetails,
27
- renderPrepareReviewCall,
28
- renderPrepareReviewResult,
29
- renderRunReviewCall,
30
- renderRunReviewResult,
31
- } from "../ui/review-tool-renderer.ts";
32
- import {
33
- type PrepareAgentReviewInput,
34
- prepareAgentReviewSchema,
35
- type RunAgentReviewInput,
36
- runAgentReviewSchema,
12
+ type PrepareReviewToolInput,
13
+ parsePrepareReviewToolInput,
14
+ parseRunReviewToolInput,
15
+ prepareReviewSchema,
16
+ runReviewSchema,
37
17
  } from "./agent-review-schemas.ts";
38
- import { prepareAgentReviewPlan, runAgentReviewBatch } from "./agent-review-workflow.ts";
39
- import { formatChildLifecycleTrace } from "./child-lifecycle-trace.ts";
40
- import {
41
- PREPARE_REVIEW_TOOL_NAME,
42
- prepareReviewPromptGuidelines,
43
- prepareReviewPromptSnippet,
44
- prepareReviewToolDescription,
45
- RUN_REVIEW_TOOL_NAME,
46
- runReviewToolDescription,
47
- } from "./guidance.ts";
48
-
49
- const TOOL_OUTPUT_CONTRACT =
50
- " Output is truncated at 2,000 lines or 50KB; full Markdown is saved to a temporary file when truncated.";
51
-
52
- /** Register the two-stage agent-driven Session-Aware Review tools. */
53
- export function registerAgentReviewTools(
54
- pi: ExtensionAPI,
55
- planStore = new ReviewPlanStore(),
56
- ): void {
57
- pi.registerTool({
58
- name: PREPARE_REVIEW_TOOL_NAME,
59
- label: "Prepare Review",
60
- description: prepareReviewToolDescription + TOOL_OUTPUT_CONTRACT,
61
- promptSnippet: prepareReviewPromptSnippet,
62
- promptGuidelines: prepareReviewPromptGuidelines,
63
- parameters: prepareAgentReviewSchema,
64
- // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
65
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
66
- const input = params as PrepareAgentReviewInput;
67
- const model = getCurrentReviewModel(ctx);
68
- if (!model) throw new Error("No current session model is available for review preparation.");
69
-
70
- const target = parseTarget(input);
71
- const sessionContext = buildSessionContext(
72
- ctx.sessionManager.getEntries(),
73
- ctx.sessionManager.getLeafId(),
74
- );
75
- const serializedContext = serializeSessionContext(sessionContext.messages);
76
- const note = input.note?.trim() || undefined;
77
-
78
- pi.events.emit("supi:working:start", { source: "supi-review" });
79
- try {
80
- const outcome = await prepareAgentReviewPlan({
81
- cwd: ctx.cwd,
82
- target,
83
- note,
84
- serializedContext,
85
- model,
86
- signal,
87
- planStore,
88
- onProgress: (progress) => {
89
- onUpdate?.({
90
- content: [{ type: "text", text: "Synthesizing review brief…" }],
91
- details: prepareProgressDetails(progress),
92
- });
93
- },
94
- });
95
- if (outcome.kind !== "prepared") {
96
- throw new Error(formatPreparationFailure(outcome));
97
- }
98
-
99
- const details: PreparedAgentReviewDetails = {
100
- kind: "review-prepared",
101
- planId: outcome.plan.id,
102
- briefPromptVersion: outcome.plan.briefPromptVersion,
103
- generatedBrief: outcome.plan.generatedBrief,
104
- snapshot: summarizeReviewSnapshot(outcome.plan.snapshot),
105
- snapshotFingerprint: outcome.plan.snapshotFingerprint,
106
- modelId: outcome.plan.model.canonicalId,
107
- };
108
- activateRunTool(pi);
109
- return toolResult(formatPreparedAgentReview(details), details, PREPARE_REVIEW_TOOL_NAME);
110
- } finally {
111
- pi.events.emit("supi:working:end", { source: "supi-review" });
112
- }
113
- },
114
- renderCall(args, theme) {
115
- return renderPrepareReviewCall(args, theme);
116
- },
117
- renderResult(result, { expanded }, theme) {
118
- return renderPrepareReviewResult(result, expanded, theme);
119
- },
120
- });
18
+ import { pageText } from "./output-page.ts";
19
+ import { prepareReview, runReview } from "./review-workflow.ts";
121
20
 
122
- pi.registerTool({
123
- name: RUN_REVIEW_TOOL_NAME,
124
- label: "Run Review",
125
- description: runReviewToolDescription + TOOL_OUTPUT_CONTRACT,
126
- parameters: runAgentReviewSchema,
127
- // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
128
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
129
- const input = params as RunAgentReviewInput;
130
- const progress = createBatchProgress(input.reviewers.map((reviewer) => reviewer.id.trim()));
131
-
132
- pi.events.emit("supi:working:start", { source: "supi-review" });
133
- try {
134
- const outcome = await runAgentReviewBatch({
135
- cwd: ctx.cwd,
136
- planId: input.planId,
137
- critique: input.critique,
138
- revisedBrief: input.revisedBrief,
139
- reviewers: input.reviewers,
140
- signal,
141
- planStore,
142
- onBriefEvaluation: (evaluation) => {
143
- recordBriefCritique(ctx.cwd, evaluation, input.reviewers.length);
144
- },
145
- onReviewerProgress: (reviewerId, reviewerProgress) => {
146
- progress.reviewers[reviewerId] = reviewerProgress;
147
- onUpdate?.({
148
- content: [
149
- {
150
- type: "text",
151
- text: `Running reviewers… ${progress.completed}/${progress.total} completed`,
152
- },
153
- ],
154
- details: batchProgressDetails(progress),
155
- });
156
- },
157
- onReviewerDone: (reviewerId) => {
158
- progress.completed++;
159
- progress.reviewers[reviewerId] = {
160
- ...(progress.reviewers[reviewerId] ?? { turns: 0, toolUses: 0 }),
161
- currentFocus: undefined,
162
- };
163
- onUpdate?.({
164
- content: [
165
- {
166
- type: "text",
167
- text: `Running reviewers… ${progress.completed}/${progress.total} completed`,
168
- },
169
- ],
170
- details: batchProgressDetails(progress),
171
- });
172
- },
173
- });
174
- if (outcome.kind !== "completed") throw new Error(outcome.reason);
175
-
176
- return toolResult(
177
- formatAgentReviewBatch(outcome.details),
178
- outcome.details,
179
- RUN_REVIEW_TOOL_NAME,
180
- );
181
- } finally {
182
- pi.events.emit("supi:working:end", { source: "supi-review" });
183
- }
184
- },
185
- renderCall(args, theme, context) {
186
- return renderRunReviewCall(args, context.expanded, theme);
187
- },
188
- renderResult(result, { expanded }, theme) {
189
- return renderRunReviewResult(result, expanded, theme);
190
- },
191
- });
192
-
193
- pi.on("session_start", () => {
194
- planStore.clear();
195
- deactivateRunTool(pi);
196
- });
197
- pi.on("session_shutdown", () => {
198
- planStore.clear();
199
- });
21
+ function target(input: PrepareReviewToolInput["target"]): ReviewTargetSpec {
22
+ return (input ?? { kind: "working-tree" }) as ReviewTargetSpec;
200
23
  }
201
24
 
202
- function parseTarget(input: PrepareAgentReviewInput): ReviewTargetSpec {
203
- const target = input.target;
204
- if (!target) return { kind: "working-tree" };
205
- if (target.kind === "working-tree") {
206
- if (target.base || target.sha) {
207
- throw new Error('target.base and target.sha must be omitted for "working-tree".');
25
+ function formatPrepared(plan: {
26
+ id: string;
27
+ snapshot: { title: string; changedFiles: string[] };
28
+ plannerDraft?: { sharedContext?: string; tasks: Array<{ id: string; instructions: string }> };
29
+ }): string {
30
+ const lines = [
31
+ "# Review Plan Prepared",
32
+ "",
33
+ `Plan ID: ${plan.id}`,
34
+ `Target: ${plan.snapshot.title}`,
35
+ `Files changed: ${plan.snapshot.changedFiles.length}`,
36
+ ];
37
+ if (plan.plannerDraft) {
38
+ lines.push("", "## Planner Draft");
39
+ if (plan.plannerDraft.sharedContext) lines.push("", plan.plannerDraft.sharedContext);
40
+ for (const task of plan.plannerDraft.tasks) {
41
+ lines.push("", `### ${task.id}`, task.instructions);
208
42
  }
209
- return { kind: "working-tree" };
43
+ lines.push("", "Call supi_review_run with an explicit accept-draft or use-review decision.");
44
+ } else {
45
+ lines.push("", "Call supi_review_run with a use-review decision containing one to four tasks.");
210
46
  }
211
- if (target.kind === "branch") {
212
- if (target.sha) throw new Error('target.sha must be omitted when target.kind is "branch".');
213
- const base = target.base?.trim();
214
- if (!base) throw new Error('target.base is required when target.kind is "branch".');
215
- if (base.startsWith("-")) {
216
- throw new Error("target.base must name a local branch, not a Git option.");
217
- }
218
- return { kind: "branch", base };
47
+ return lines.join("\n");
48
+ }
49
+
50
+ function formatTaskResult(result: ReviewTaskResult): string[] {
51
+ const lines = [
52
+ "",
53
+ `## ${result.taskId}`,
54
+ `Model: ${result.modelId}`,
55
+ `Packet SHA-256: ${result.packetHash}`,
56
+ ];
57
+ if (result.status === "failed") return [...lines, `Status: failed (${result.failureCode})`];
58
+ if (result.status === "canceled") return [...lines, "Status: canceled"];
59
+ if (result.status === "timeout") {
60
+ return [...lines, `Status: timeout (${result.timeoutMs} ms)`];
219
61
  }
220
- if (target.base) throw new Error('target.base must be omitted when target.kind is "commit".');
221
- const sha = target.sha?.trim();
222
- if (!sha) throw new Error('target.sha is required when target.kind is "commit".');
223
- if (!isCommitObjectId(sha)) {
224
- throw new Error("target.sha must be a hexadecimal commit object id (7–64 characters).");
62
+ lines.push(`Verdict: ${result.verdict.toUpperCase()}`, "", result.summary);
63
+ for (const finding of result.findings) {
64
+ lines.push(
65
+ "",
66
+ `- ${finding.title} [${finding.blocksAcceptance ? "blocking" : "non-blocking"}; impact ${finding.impact}; effort ${finding.effort}; confidence ${finding.confidence}]`,
67
+ ...(finding.location
68
+ ? [
69
+ ` Location: ${finding.location.path}:${finding.location.startLine}-${finding.location.endLine}`,
70
+ ]
71
+ : []),
72
+ ` ${finding.description}`,
73
+ );
225
74
  }
226
- return { kind: "commit", sha };
75
+ return lines;
227
76
  }
228
77
 
229
- function formatPreparationFailure(
230
- outcome: Exclude<Awaited<ReturnType<typeof prepareAgentReviewPlan>>, { kind: "prepared" }>,
231
- ): string {
232
- if (outcome.kind === "no-snapshot") return outcome.reason;
233
-
234
- const { result } = outcome;
235
- const trace = result.diagnostics?.lifecycleTrace;
236
- return trace
237
- ? `${formatBriefSynthesisFailureCopy(result)}\n\n${formatChildLifecycleTrace(trace)}`
238
- : formatBriefSynthesisFailureCopy(result);
78
+ export function formatReviewBatch(details: ReviewBatchDetails): string {
79
+ const lines = [
80
+ "# Review Complete",
81
+ "",
82
+ `Mode: ${details.mode}`,
83
+ `Provenance: ${details.provenance}`,
84
+ `Target: ${details.snapshot.title}`,
85
+ ];
86
+ if (details.planning) {
87
+ lines.push(
88
+ `Planner: ${details.planning.modelId} (protocol ${details.planning.promptVersion})`,
89
+ `Planner decision: ${details.planning.decision}`,
90
+ );
91
+ }
92
+ for (const result of details.results) lines.push(...formatTaskResult(result));
93
+ return lines.join("\n");
239
94
  }
240
95
 
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]);
96
+ function resolveModels(ctx: Parameters<Parameters<ExtensionAPI["registerTool"]>[0]["execute"]>[4]) {
97
+ const config = loadReviewConfig(ctx.cwd);
98
+ const reviewer = resolveAgentReviewModel(ctx, config.agentModel);
99
+ const planner = resolveAgentReviewModel(ctx, config.plannerModel);
100
+ if (!reviewer)
101
+ throw new Error(`Configured reviewer model "${config.agentModel}" is unavailable.`);
102
+ return { reviewer, planner, config };
245
103
  }
246
104
 
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
- }
105
+ /**
106
+ * Wraps onUpdate to also update a StatusSpinner with progress counts.
107
+ * Returns the adapted callback that both updates the spinner and calls onUpdate.
108
+ */
109
+ function wireSpinnerToProgress(
110
+ // biome-ignore lint/suspicious/noExplicitAny: tool execute ctx type is narrower than ExtensionContext
111
+ ctx: any,
112
+ // biome-ignore lint/suspicious/noExplicitAny: onUpdate callback type varies per tool
113
+ onUpdate: any,
114
+ startMessage = "Reviewing…",
115
+ ): {
116
+ statusSpinner: StatusSpinner;
117
+ wrappedUpdate: (result: {
118
+ content: Array<{ type: "text"; text: string }>;
119
+ details: Record<string, unknown>;
120
+ }) => void;
121
+ } {
122
+ const statusSpinner = new StatusSpinner(ctx, "supi-review");
123
+ statusSpinner.start(startMessage);
252
124
 
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 },
125
+ let completedCount = 0;
126
+ let totalCount = 0;
127
+ const wrappedUpdate = (result: {
128
+ content: Array<{ type: "text"; text: string }>;
129
+ details: Record<string, unknown>;
130
+ }) => {
131
+ const details = result.details as { completedCount?: number; totalCount?: number };
132
+ completedCount = details.completedCount ?? completedCount;
133
+ totalCount = details.totalCount ?? totalCount;
134
+ const label =
135
+ totalCount > 0
136
+ ? `Reviewing… (${completedCount} of ${totalCount} tasks complete)`
137
+ : "Reviewing…";
138
+ statusSpinner.update(label);
139
+ onUpdate?.(result);
260
140
  };
141
+ return { statusSpinner, wrappedUpdate };
261
142
  }
262
143
 
263
- interface BatchProgressState {
264
- completed: number;
265
- total: number;
266
- reviewers: Record<string, ReviewProgress>;
144
+ /** Compute the initial task count for the progress indicator. */
145
+ function initialTaskCount(
146
+ input: ReturnType<typeof parseRunReviewToolInput>,
147
+ planStore: ReviewPlanStore,
148
+ ): number {
149
+ if (input.mode === "direct") return input.review?.tasks?.length ?? 1;
150
+ if (input.decision?.kind === "use-review") return input.decision.review?.tasks?.length ?? 0;
151
+ if (input.decision?.kind === "accept-draft") {
152
+ return planStore.peek(input.planId)?.plannerDraft?.tasks.length ?? 0;
153
+ }
154
+ return 0;
267
155
  }
268
156
 
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
- ),
157
+ /** Factory for the supi_review_run execute function with animated status-bar spinner. */
158
+ function makeRunReviewExecute(
159
+ planStore: ReviewPlanStore,
160
+ ): NonNullable<Parameters<ExtensionAPI["registerTool"]>[0]["execute"]> {
161
+ // biome-ignore lint/complexity/useMaxParams: Pi ToolDefinition execute signature
162
+ return async (_id, params, signal, onUpdate, ctx) => {
163
+ const input = parseRunReviewToolInput(params);
164
+
165
+ const { statusSpinner, wrappedUpdate } = wireSpinnerToProgress(ctx, onUpdate);
166
+
167
+ const taskCount = initialTaskCount(input, planStore);
168
+ wrappedUpdate({
169
+ content: [{ type: "text", text: "Starting review…" }],
170
+ details: taskCount > 0 ? { completedCount: 0, totalCount: taskCount } : {},
171
+ });
172
+
173
+ try {
174
+ const outcome =
175
+ input.mode === "direct"
176
+ ? await runReview({
177
+ mode: "direct",
178
+ cwd: ctx.cwd,
179
+ target: input.target,
180
+ review: input.review,
181
+ reviewerModel: resolveModels(ctx).reviewer,
182
+ signal,
183
+ onUpdate: wrappedUpdate,
184
+ })
185
+ : await runReview({
186
+ mode: "prepared",
187
+ cwd: ctx.cwd,
188
+ planId: input.planId,
189
+ decision: input.decision,
190
+ planStore,
191
+ signal,
192
+ onUpdate: wrappedUpdate,
193
+ });
194
+ if (outcome.kind !== "completed") throw new Error(outcome.reason);
195
+ return {
196
+ content: [{ type: "text", text: pageText(formatReviewBatch(outcome.details)).text }],
197
+ details: outcome.details,
198
+ };
199
+ } finally {
200
+ statusSpinner.stop();
201
+ }
276
202
  };
277
203
  }
278
204
 
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
- };
205
+ /** Throw an actionable error from a non-prepared outcome. */
206
+ function throwPrepareFailure(
207
+ outcome: { kind: string; reason?: string } & Record<string, unknown>,
208
+ ): never {
209
+ if (outcome.kind === "no-target")
210
+ throw new Error(outcome.reason ?? "No reviewable changes found.");
211
+ const result = outcome.result as { kind: string; failureCode?: string; timeoutMs?: number };
212
+ const diagnostic =
213
+ result.kind === "failed"
214
+ ? result.failureCode
215
+ : result.kind === "timeout"
216
+ ? `timeout (${result.timeoutMs} ms)`
217
+ : result.kind;
218
+ throw new Error(`Planner failed: ${diagnostic}`);
287
219
  }
288
220
 
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,
221
+ /** Register optional preparation and universal direct/prepared execution tools. */
222
+ export function registerAgentReviewTools(pi: ExtensionAPI, planStore: ReviewPlanStore): void {
223
+ pi.registerTool({
224
+ name: "supi_review_prepare",
225
+ label: "Prepare Review",
226
+ description: "Create a one-shot Review Plan, optionally with a lightweight Planner Draft.",
227
+ promptSnippet: "Prepare an optional one-shot code review plan",
228
+ promptGuidelines: [
229
+ "Use supi_review_prepare only when a caller wants to inspect or request a Planner Draft before execution.",
230
+ "Use supi_review_run in direct mode when the current skill or agent already knows the review tasks.",
231
+ ],
232
+ parameters: prepareReviewSchema,
233
+ renderCall: renderPrepareCall,
234
+ renderResult: renderPrepareResult,
235
+ // biome-ignore lint/complexity/useMaxParams: Pi ToolDefinition execute signature
236
+ async execute(_id, params, signal, onUpdate, ctx) {
237
+ const input = parsePrepareReviewToolInput(params);
238
+ const models = resolveModels(ctx);
239
+ if ((input.planning ?? "none") === "suggest" && !models.planner) {
240
+ throw new Error(`Configured Planner model "${models.config.plannerModel}" is unavailable.`);
241
+ }
242
+
243
+ const { statusSpinner, wrappedUpdate } = wireSpinnerToProgress(
244
+ ctx,
245
+ onUpdate,
246
+ "Preparing review…",
247
+ );
248
+
249
+ try {
250
+ const session = buildSessionContext(
251
+ ctx.sessionManager.getEntries(),
252
+ ctx.sessionManager.getLeafId(),
253
+ );
254
+ const outcome = await prepareReview({
255
+ cwd: ctx.cwd,
256
+ target: target(input.target),
257
+ planning: input.planning ?? "none",
258
+ plannerContext: collectPlannerContext(session.messages),
259
+ reviewerModel: models.reviewer,
260
+ plannerModel: models.planner,
261
+ planStore,
262
+ signal,
263
+ onUpdate: wrappedUpdate,
264
+ });
265
+ if (outcome.kind !== "prepared") throwPrepareFailure(outcome);
266
+ return {
267
+ content: [{ type: "text", text: pageText(formatPrepared(outcome.plan)).text }],
268
+ details: {
269
+ kind: "review-prepared",
270
+ planId: outcome.plan.id,
271
+ snapshot: summarizeReviewSnapshot(outcome.plan.snapshot),
272
+ reviewerModelId: outcome.plan.reviewerModel.canonicalId,
273
+ plannerDraft: outcome.plan.plannerDraft,
274
+ plannerModelId: outcome.plan.plannerModelId,
275
+ plannerPromptVersion: outcome.plan.plannerPromptVersion,
276
+ },
277
+ };
278
+ } finally {
279
+ statusSpinner.stop();
280
+ }
308
281
  },
309
- rawData: evaluation,
310
282
  });
311
- }
312
283
 
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,
284
+ pi.registerTool({
285
+ name: "supi_review_run",
286
+ label: "Run Review",
287
+ description: "Run one to four caller-defined review tasks directly or from a prepared plan.",
288
+ promptSnippet: "Run independent read-only code review tasks",
289
+ promptGuidelines: [
290
+ "Use supi_review_run directly when a skill or the main agent already has complete review task instructions.",
291
+ "Repository stability from invocation through completion is a caller precondition for supi_review_run.",
292
+ ],
293
+ parameters: runReviewSchema,
294
+ renderCall: renderRunCall,
295
+ renderResult: renderRunResult,
296
+ execute: makeRunReviewExecute(planStore),
317
297
  });
318
- if (!truncation.truncated) {
319
- return { content: [{ type: "text" as const, text: truncation.content }], details };
320
- }
321
298
 
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
- };
299
+ pi.on("session_start", () => planStore.clear());
300
+ pi.on("session_shutdown", () => planStore.clear());
333
301
  }
@@ -65,7 +65,7 @@ export function createEarlyCancellationDiagnostics(): ChildFailureDiagnostics {
65
65
 
66
66
  /** Generate static parent-facing copy for a host-owned child failure code. */
67
67
  export function formatChildFailureCopy(stage: ChildStage, code: ChildFailureCode): string {
68
- const label = stage === "brief-synthesis" ? "Brief synthesis" : "Reviewer";
68
+ const label = stage === "planner" ? "Planner" : "Reviewer";
69
69
  switch (code) {
70
70
  case "session-creation-failed":
71
71
  return `${label} session could not be created.`;
@@ -0,0 +1,37 @@
1
+ import { DefaultResourceLoader, SettingsManager } from "@earendil-works/pi-coding-agent";
2
+
3
+ /** Loader and settings manager preconfigured for an isolated child session. */
4
+ export interface IsolatedChildResources {
5
+ loader: DefaultResourceLoader;
6
+ settingsManager: SettingsManager;
7
+ }
8
+
9
+ /**
10
+ * Build child-session resources with no ambient settings or discovered prompt
11
+ * content. Compaction and retries stay disabled so provider limits apply to the
12
+ * original packet directly.
13
+ */
14
+ export function createIsolatedChildResources(
15
+ cwd: string,
16
+ protocolPrompt: string,
17
+ agentDir = process.env.PI_CODING_AGENT_DIR || "",
18
+ ): IsolatedChildResources {
19
+ const settingsManager = SettingsManager.inMemory({
20
+ compaction: { enabled: false },
21
+ retry: { enabled: false },
22
+ });
23
+ const loader = new DefaultResourceLoader({
24
+ cwd,
25
+ agentDir,
26
+ settingsManager,
27
+ noExtensions: true,
28
+ noSkills: true,
29
+ noPromptTemplates: true,
30
+ noThemes: true,
31
+ noContextFiles: true,
32
+ appendSystemPrompt: [protocolPrompt],
33
+ systemPromptOverride: () => undefined,
34
+ appendSystemPromptOverride: () => [protocolPrompt],
35
+ });
36
+ return { loader, settingsManager };
37
+ }