@mrclrchtr/supi-review 1.6.0 → 1.8.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 CHANGED
@@ -8,7 +8,7 @@ Adds an interactive `/supi-review` command to the [pi coding agent](https://gith
8
8
  pi install npm:@mrclrchtr/supi-review
9
9
  ```
10
10
 
11
- This is a **beta** package. It is not bundled in `@mrclrchtr/supi`.
11
+ This is a **beta** package. Install individually.
12
12
 
13
13
  For local development:
14
14
 
@@ -27,7 +27,7 @@ After install, pi gets one command:
27
27
  The reviewer runs in managed child agent sessions:
28
28
 
29
29
  - a **brief synthesizer** creates a structured review brief from the active session branch
30
- - a **read-only reviewer** inspects the selected snapshot and submits structured findings
30
+ - a **read-only reviewer** inspects the selected snapshot (without receiving bulk inline diffs) and submits structured findings
31
31
 
32
32
  ## Review flow
33
33
 
@@ -38,8 +38,8 @@ The reviewer runs in managed child agent sessions:
38
38
  3. optionally add a short note
39
39
  4. resolve the snapshot
40
40
  5. synthesize a review brief from the current session history
41
- 6. preview the synthesized brief + prompt coverage
42
- 7. run the review with a live progress widget
41
+ 6. preview the synthesized brief + compact prompt preview
42
+ 7. the reviewer fetches per-file diffs on demand via snapshot-aware tools; live progress widget shows activity
43
43
  8. show the structured result as a custom message
44
44
  9. if findings exist, hand off to the main agent so it can ask what to do next
45
45
 
@@ -58,7 +58,8 @@ The generated review prompt is **not** just a static diff wrapper.
58
58
  Before the actual review starts, the package:
59
59
 
60
60
  - resolves the **active session branch into the current LLM-visible context**
61
- - extracts high-signal user/assistant/custom/compaction context from that resolved view
61
+ - **serializes** that resolved context into a compaction-style transcript
62
+ - feeds the serialized transcript (plus snapshot + optional note) to a dedicated brief synthesizer
62
63
  - synthesizes a structured brief with:
63
64
  - summary
64
65
  - intended outcome
@@ -69,7 +70,9 @@ Before the actual review starts, the package:
69
70
 
70
71
  The synthesizer also receives a bounded diff excerpt from the snapshot so it can reason about actual code changes, not just filenames.
71
72
 
72
- That synthesized brief is then combined with the git snapshot into the final reviewer prompt.
73
+ That synthesized brief is then combined with the git snapshot into a compact reviewer prompt. The prompt contains the brief, file manifest, and per-file overview, but no large inline diffs. Instead, the reviewer session gets snapshot-aware tools (`read_snapshot_diff`, `read_snapshot_file`) to fetch exact per-file diffs and before/after file contents on demand.
74
+
75
+ The session-transcript approach mirrors how Pi summarizes context for compaction: the entire resolved conversation is rendered in a readable label format and sent to the model as a whole, rather than relying on heuristic excerpt ranking.
73
76
 
74
77
  ## Model selection
75
78
 
@@ -104,9 +107,10 @@ When a successful review contains findings, `supi-review` also injects an agent-
104
107
  - `src/review.ts` — command orchestration and interactive flow
105
108
  - `src/model.ts` — explicit model selection helpers
106
109
  - `src/git.ts` — git snapshot resolution
107
- - `src/history/collect.ts` — active-branch evidence extraction
110
+ - `src/history/collect.ts` — compaction-style session-context serialization
108
111
  - `src/history/synthesize.ts` — brief synthesis orchestration
109
112
  - `src/target/packet.ts` — final reviewer packet builder
110
113
  - `src/tool/brief-runner.ts` — brief synthesis child session
111
- - `src/tool/review-runner.ts` — read-only reviewer child session
114
+ - `src/tool/review-runner.ts` — read-only reviewer child session with snapshot-aware tools
115
+ - `src/tool/snapshot-tools.ts` — per-file diff and before/after content tools scoped to the selected snapshot
112
116
  - `src/ui/renderer.ts` — structured result rendering
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-review",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "SuPi Review extension — structured code review via /supi-review command",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/git.ts CHANGED
@@ -1,10 +1,11 @@
1
+ // biome-ignore lint/nursery/noExcessiveLinesPerFile: many tightly-coupled git helpers; splitting would create cross-ref overhead
1
2
  import { execFile } from "node:child_process";
2
- import { basename } from "node:path";
3
+ import { readFile } from "node:fs/promises";
4
+ import { basename, join } from "node:path";
3
5
  import { promisify } from "node:util";
4
- import type { DiffStats, ReviewSnapshot } from "./types.ts";
6
+ import type { DiffStats, ReviewSnapshot, ReviewTargetSpec } from "./types.ts";
5
7
 
6
8
  const execFileAsync = promisify(execFile);
7
-
8
9
  const GIT_TIMEOUT_MS = 30_000;
9
10
 
10
11
  function scrubGitEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
@@ -237,11 +238,7 @@ export async function resolveWorkingTreeSnapshot(
237
238
  getUncommittedDiff(repoPath),
238
239
  getUncommittedFileNames(repoPath),
239
240
  ]);
240
-
241
- if (!diffText.trim() && changedFiles.length === 0) {
242
- return undefined;
243
- }
244
-
241
+ if (!diffText.trim() && changedFiles.length === 0) return undefined;
245
242
  return {
246
243
  target: { kind: "working-tree" },
247
244
  title: "Working tree changes",
@@ -257,19 +254,12 @@ export async function resolveBranchSnapshot(
257
254
  base: string,
258
255
  ): Promise<ReviewSnapshot | undefined> {
259
256
  const baseSha = await getMergeBase(repoPath, base);
260
- if (!baseSha) {
261
- return undefined;
262
- }
263
-
257
+ if (!baseSha) return undefined;
264
258
  const [diffText, changedFiles] = await Promise.all([
265
259
  getDiff(repoPath, baseSha),
266
260
  getDiffFileNames(repoPath, baseSha),
267
261
  ]);
268
-
269
- if (!diffText.trim() && changedFiles.length === 0) {
270
- return undefined;
271
- }
272
-
262
+ if (!diffText.trim() && changedFiles.length === 0) return undefined;
273
263
  return {
274
264
  target: { kind: "branch", base },
275
265
  title: `Changes vs ${base}`,
@@ -288,11 +278,7 @@ export async function resolveCommitSnapshot(
288
278
  getCommitShow(repoPath, sha),
289
279
  getCommitFileNames(repoPath, sha),
290
280
  ]);
291
-
292
- if (!diffText.trim() && changedFiles.length === 0) {
293
- return undefined;
294
- }
295
-
281
+ if (!diffText.trim() && changedFiles.length === 0) return undefined;
296
282
  return {
297
283
  target: { kind: "commit", sha },
298
284
  title: `Commit ${sha.slice(0, 7)}`,
@@ -302,6 +288,122 @@ export async function resolveCommitSnapshot(
302
288
  };
303
289
  }
304
290
 
291
+ /** Get the per-file diff for a single changed file in the snapshot. */
292
+ export async function getSnapshotFileDiff(
293
+ repoPath: string,
294
+ snapshot: ReviewSnapshot,
295
+ file: string,
296
+ ): Promise<string> {
297
+ const { target } = snapshot;
298
+ switch (target.kind) {
299
+ case "working-tree": {
300
+ const { stdout } = await execFileAsync(
301
+ "git",
302
+ ["diff", "HEAD", "--", file],
303
+ gitExecOptions(repoPath),
304
+ );
305
+ return stdout;
306
+ }
307
+ case "branch": {
308
+ const baseSha = await getMergeBase(repoPath, target.base);
309
+ if (!baseSha) return "";
310
+ const { stdout } = await execFileAsync(
311
+ "git",
312
+ ["diff", baseSha, "HEAD", "--", file],
313
+ gitExecOptions(repoPath),
314
+ );
315
+ return stdout;
316
+ }
317
+ case "commit": {
318
+ const { stdout } = await execFileAsync(
319
+ "git",
320
+ ["show", target.sha, "--", file],
321
+ gitExecOptions(repoPath),
322
+ );
323
+ return stdout;
324
+ }
325
+ }
326
+ }
327
+
328
+ /** Run `git show <ref>:<file>` and return the blob content. */
329
+ async function showGitBlob(repoPath: string, ref: string, file: string): Promise<string> {
330
+ const { stdout } = await execFileAsync(
331
+ "git",
332
+ ["show", `${ref}:${file}`],
333
+ gitExecOptions(repoPath),
334
+ );
335
+ return stdout;
336
+ }
337
+
338
+ async function resolveWorkingTreeContent(
339
+ repoPath: string,
340
+ file: string,
341
+ side: "before" | "after",
342
+ ): Promise<string | undefined> {
343
+ if (side === "before") {
344
+ try {
345
+ return await showGitBlob(repoPath, "HEAD", file);
346
+ } catch {
347
+ return undefined;
348
+ }
349
+ }
350
+ try {
351
+ return await readFile(join(repoPath, file), "utf-8");
352
+ } catch {
353
+ return undefined;
354
+ }
355
+ }
356
+
357
+ async function resolveBranchContent(
358
+ repoPath: string,
359
+ target: ReviewTargetSpec & { kind: "branch" },
360
+ file: string,
361
+ side: "before" | "after",
362
+ ): Promise<string | undefined> {
363
+ const baseSha = await getMergeBase(repoPath, target.base);
364
+ if (!baseSha) return undefined;
365
+ const ref = side === "before" ? baseSha : "HEAD";
366
+ try {
367
+ return await showGitBlob(repoPath, ref, file);
368
+ } catch (err) {
369
+ if (side === "before") return undefined;
370
+ throw err;
371
+ }
372
+ }
373
+
374
+ async function resolveCommitContent(
375
+ repoPath: string,
376
+ target: ReviewTargetSpec & { kind: "commit" },
377
+ file: string,
378
+ side: "before" | "after",
379
+ ): Promise<string | undefined> {
380
+ const ref = side === "before" ? `${target.sha}^` : target.sha;
381
+ try {
382
+ return await showGitBlob(repoPath, ref, file);
383
+ } catch (err) {
384
+ if (side === "before") return undefined;
385
+ throw err;
386
+ }
387
+ }
388
+
389
+ /** Get before or after content for a single changed file in the snapshot. Returns undefined when legitimately unavailable; propagates unexpected errors. */
390
+ export async function getSnapshotFileContent(
391
+ repoPath: string,
392
+ snapshot: ReviewSnapshot,
393
+ file: string,
394
+ side: "before" | "after",
395
+ ): Promise<string | undefined> {
396
+ const { target } = snapshot;
397
+ switch (target.kind) {
398
+ case "working-tree":
399
+ return resolveWorkingTreeContent(repoPath, file, side);
400
+ case "branch":
401
+ return resolveBranchContent(repoPath, target, file, side);
402
+ case "commit":
403
+ return resolveCommitContent(repoPath, target, file, side);
404
+ }
405
+ }
406
+
305
407
  /** Convenience label for one changed file, used in synthesized prompts/UI. */
306
408
  export function formatChangedFileLabel(file: string): string {
307
409
  return basename(file) === file ? file : `${basename(file)} (${file})`;
@@ -1,114 +1,134 @@
1
1
  import type { SessionContext } from "@earendil-works/pi-coding-agent";
2
- import type { HistoryEvidence, ReviewSnapshot } from "../types.ts";
3
-
4
- const INTENT_PATTERN =
5
- /\b(fix|refactor|rename|preserve|avoid|should|must|ensure|intended|goal|risk|concern|regression|security|correctness|performance|api|breaking|review)\b/i;
6
2
 
7
3
  type ResolvedSessionMessage = SessionContext["messages"][number];
8
4
 
5
+ const DEFAULT_MAX_CHARS = 8_000;
6
+
7
+ export interface SerializeSessionContextOptions {
8
+ maxChars?: number;
9
+ }
10
+
9
11
  /**
10
- * Extract the highest-signal evidence from the resolved LLM-visible session context.
12
+ * Serialize the resolved LLM-visible session context into a compaction-style
13
+ * readable transcript for the brief synthesizer.
11
14
  *
12
- * The collector intentionally favors user intent, assistant plans, compaction
13
- * summaries, and custom extension messages over raw tool chatter.
15
+ * This mirrors the overall approach Pi uses for compaction:
16
+ * - messages are labeled by role and rendered in chronological order
17
+ * - compaction and branch summaries appear with their own labels
18
+ * - the output is bounded so the synthesizer stays predictable on long sessions
19
+ * - no heuristic ranking or scoring is applied
14
20
  */
15
- export function collectHistoryEvidence(
21
+ export function serializeSessionContext(
16
22
  messages: ResolvedSessionMessage[],
17
- snapshot: ReviewSnapshot,
18
- note?: string,
19
- ): HistoryEvidence[] {
20
- const scoringContext = {
21
- changedPathTokens: collectPathTokens(snapshot.changedFiles),
22
- noteTokens: collectFreeformTokens(note),
23
- total: messages.length,
24
- };
25
-
26
- const scored = messages
27
- .map((message) => toEvidence(message))
23
+ options?: SerializeSessionContextOptions,
24
+ ): string {
25
+ const maxChars = options?.maxChars ?? DEFAULT_MAX_CHARS;
26
+ if (messages.length === 0) return "";
27
+
28
+ const entries = messages
29
+ .map((message) => serializeEntry(message))
28
30
  .filter(
29
- (evidence): evidence is Omit<HistoryEvidence, "score" | "reason"> => evidence !== undefined,
30
- )
31
- .map((evidence, index) => scoreEvidence(evidence, index, scoringContext))
32
- .sort((a, b) => b.score - a.score || b.text.length - a.text.length);
31
+ (entry): entry is { label: string; text: string; isSummary: boolean } => entry !== undefined,
32
+ );
33
+
34
+ if (entries.length === 0) return "";
35
+
36
+ // Separate summary entries (compaction/branch summaries) from regular entries
37
+ const summaryEntries = entries.filter((e) => e.isSummary);
38
+ const regularEntries = entries.filter((e) => !e.isSummary);
39
+
40
+ // Estimate the total size if we keep everything
41
+ const allLines = entries.map(formatEntry);
42
+ const totalSize = measureLines(allLines);
43
+
44
+ if (totalSize <= maxChars) {
45
+ return allLines.join("\n");
46
+ }
47
+
48
+ // Build the output: always include summaries, then as many recent regular entries as fit
49
+ const summaryLines = summaryEntries.map(formatEntry);
50
+
51
+ // Walk regular entries from newest to oldest, keeping as many as fit
52
+ const keptRegular: typeof regularEntries = [];
53
+ let size = measureLines(summaryLines);
54
+
55
+ for (let i = regularEntries.length - 1; i >= 0; i--) {
56
+ const line = formatEntry(regularEntries[i]);
57
+ const lineLen = line.length + 1; // +1 for newline
58
+ if (keptRegular.length > 0 && size + lineLen > maxChars) {
59
+ break;
60
+ }
61
+ keptRegular.unshift(regularEntries[i]);
62
+ size += lineLen;
63
+ }
64
+
65
+ // Build final output: summaries first, then kept recent entries
66
+ const keptLines = [...summaryLines, ...keptRegular.map(formatEntry)];
33
67
 
34
- return capEvidence(scored, 10, 4_500);
68
+ // If still nothing fits with summaries alone, truncate the summaries
69
+ if (keptLines.length > 0) {
70
+ const output = keptLines.join("\n");
71
+ if (output.length <= maxChars) return output;
72
+ }
73
+
74
+ // Last resort: truncate the full output
75
+ return keepFirstLines(keptLines, maxChars);
76
+ }
77
+
78
+ function formatEntry(entry: { label: string; text: string }): string {
79
+ return `[${entry.label}]\n${entry.text}`;
35
80
  }
36
81
 
37
- function toEvidence(
82
+ function measureLines(lines: string[]): number {
83
+ return lines.reduce((acc, line) => acc + line.length + 1, 0); // +1 for newline
84
+ }
85
+
86
+ function keepFirstLines(lines: string[], maxChars: number): string {
87
+ const kept: string[] = [];
88
+ let size = 0;
89
+ for (const line of lines) {
90
+ const lineLen = line.length + 1;
91
+ if (size + lineLen > maxChars) break;
92
+ kept.push(line);
93
+ size += lineLen;
94
+ }
95
+ return kept.join("\n");
96
+ }
97
+
98
+ function serializeEntry(
38
99
  message: ResolvedSessionMessage,
39
- ): Omit<HistoryEvidence, "score" | "reason"> | undefined {
100
+ ): { label: string; text: string; isSummary: boolean } | undefined {
40
101
  switch (message.role) {
41
102
  case "user":
42
103
  case "assistant": {
104
+ const text = normalizeText(extractMessageText(message.content));
105
+ if (!text) return undefined;
43
106
  return {
44
- kind: message.role,
45
- text: normalizeText(extractMessageText(message.content)),
107
+ label: message.role === "user" ? "User" : "Assistant",
108
+ text,
109
+ isSummary: false,
46
110
  };
47
111
  }
48
112
  case "custom": {
49
- return {
50
- kind: "custom",
51
- text: normalizeText(extractMessageText(message.content)),
52
- };
113
+ const text = normalizeText(extractMessageText(message.content));
114
+ if (!text) return undefined;
115
+ return { label: "Custom", text, isSummary: false };
53
116
  }
54
117
  case "compactionSummary": {
55
- return {
56
- kind: "compaction",
57
- text: normalizeText(message.summary),
58
- };
118
+ const text = normalizeText(message.summary);
119
+ if (!text) return undefined;
120
+ return { label: "Compaction summary", text, isSummary: true };
59
121
  }
60
122
  case "branchSummary": {
61
- return {
62
- kind: "branch-summary",
63
- text: normalizeText(message.summary),
64
- };
123
+ const text = normalizeText(message.summary);
124
+ if (!text) return undefined;
125
+ return { label: "Branch summary", text, isSummary: true };
65
126
  }
66
127
  default:
67
128
  return undefined;
68
129
  }
69
130
  }
70
131
 
71
- function scoreEvidence(
72
- evidence: Omit<HistoryEvidence, "score" | "reason">,
73
- index: number,
74
- context: { total: number; changedPathTokens: string[]; noteTokens: string[] },
75
- ): HistoryEvidence {
76
- const recencyBoost = context.total > 0 ? (index / context.total) * 3 : 0;
77
- const pathMatches = countTokenMatches(evidence.text, context.changedPathTokens);
78
- const noteMatches = countTokenMatches(evidence.text, context.noteTokens);
79
- const intentBoost = INTENT_PATTERN.test(evidence.text) ? 4 : 0;
80
- const shortTextPenalty = evidence.text.length < 24 ? -2 : 0;
81
- const longTextPenalty = evidence.text.length > 900 ? -1 : 0;
82
- const score =
83
- baseKindScore(evidence.kind) +
84
- recencyBoost +
85
- pathMatches * 3 +
86
- noteMatches * 2 +
87
- intentBoost +
88
- shortTextPenalty +
89
- longTextPenalty;
90
-
91
- return {
92
- ...evidence,
93
- score,
94
- reason: buildReason(pathMatches, noteMatches, intentBoost),
95
- };
96
- }
97
-
98
- function buildReason(pathMatches: number, noteMatches: number, intentBoost: number): string {
99
- const reasons: string[] = [];
100
- if (pathMatches > 0) {
101
- reasons.push(`mentions ${pathMatches} changed-path token${pathMatches === 1 ? "" : "s"}`);
102
- }
103
- if (noteMatches > 0) {
104
- reasons.push(`matches ${noteMatches} note token${noteMatches === 1 ? "" : "s"}`);
105
- }
106
- if (intentBoost > 0) {
107
- reasons.push("contains intent/constraint language");
108
- }
109
- return reasons.join(", ") || "high-signal session context";
110
- }
111
-
112
132
  function extractMessageText(content: unknown): string {
113
133
  if (typeof content === "string") {
114
134
  return content;
@@ -131,80 +151,3 @@ function extractMessageText(content: unknown): string {
131
151
  function normalizeText(text: string): string {
132
152
  return text.replace(/\s+/g, " ").trim();
133
153
  }
134
-
135
- function collectPathTokens(paths: string[]): string[] {
136
- const tokens = new Set<string>();
137
-
138
- for (const path of paths) {
139
- for (const part of path.split(/[\\/]/)) {
140
- addToken(tokens, part);
141
- for (const inner of part.split(/[-_.]/)) {
142
- addToken(tokens, inner);
143
- }
144
- }
145
- addToken(tokens, path);
146
- }
147
-
148
- return Array.from(tokens);
149
- }
150
-
151
- function collectFreeformTokens(text: string | undefined): string[] {
152
- if (!text) return [];
153
-
154
- const tokens = new Set<string>();
155
- for (const part of text.split(/\W+/)) {
156
- addToken(tokens, part);
157
- }
158
- return Array.from(tokens);
159
- }
160
-
161
- function addToken(tokens: Set<string>, token: string): void {
162
- const normalized = token.trim().toLowerCase();
163
- if (normalized.length < 3) return;
164
- tokens.add(normalized);
165
- }
166
-
167
- function countTokenMatches(text: string, tokens: string[]): number {
168
- if (tokens.length === 0 || text.length === 0) return 0;
169
- const lower = text.toLowerCase();
170
- return tokens.reduce((count, token) => count + (lower.includes(token) ? 1 : 0), 0);
171
- }
172
-
173
- function baseKindScore(kind: HistoryEvidence["kind"]): number {
174
- switch (kind) {
175
- case "user":
176
- return 8;
177
- case "assistant":
178
- return 5;
179
- case "compaction":
180
- case "branch-summary":
181
- return 6;
182
- case "custom":
183
- return 4;
184
- }
185
- }
186
-
187
- function capEvidence(
188
- evidence: HistoryEvidence[],
189
- maxItems: number,
190
- maxChars: number,
191
- ): HistoryEvidence[] {
192
- const kept: HistoryEvidence[] = [];
193
- let used = 0;
194
-
195
- for (const item of evidence) {
196
- if (kept.length >= maxItems) break;
197
- const text = truncate(item.text, 600);
198
- const nextUsed = used + text.length;
199
- if (kept.length > 0 && nextUsed > maxChars) break;
200
- kept.push({ ...item, text });
201
- used = nextUsed;
202
- }
203
-
204
- return kept;
205
- }
206
-
207
- function truncate(text: string, maxChars: number): string {
208
- if (text.length <= maxChars) return text;
209
- return `${text.slice(0, maxChars)}…`;
210
- }
@@ -1,7 +1,7 @@
1
1
  import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
2
2
  import { runBriefSynthesis } from "../tool/brief-runner.ts";
3
3
  import type { BriefSynthesisRunResult, ReviewProgress } from "../tool/runner-types.ts";
4
- import type { HistoryEvidence, ReviewModelSelection, ReviewSnapshot } from "../types.ts";
4
+ import type { ReviewModelSelection, ReviewSnapshot } from "../types.ts";
5
5
 
6
6
  const DIFF_EXCERPT_CHAR_BUDGET = 12_000;
7
7
 
@@ -10,20 +10,21 @@ export interface SynthesizeReviewBriefOptions {
10
10
  modelRegistry: ModelRegistry;
11
11
  cwd: string;
12
12
  snapshot: ReviewSnapshot;
13
- evidence: HistoryEvidence[];
13
+ serializedContext: string;
14
14
  note?: string;
15
15
  signal?: AbortSignal;
16
16
  onProgress?: (progress: ReviewProgress) => void;
17
17
  }
18
18
 
19
- /** Synthesize a structured review brief from the current snapshot and session evidence. */
19
+ /** Synthesize a structured review brief from the current snapshot and session context. */
20
20
  export function synthesizeReviewBrief(
21
21
  options: SynthesizeReviewBriefOptions,
22
22
  ): Promise<BriefSynthesisRunResult> {
23
- const { model, modelRegistry, cwd, snapshot, evidence, note, signal, onProgress } = options;
23
+ const { model, modelRegistry, cwd, snapshot, serializedContext, note, signal, onProgress } =
24
+ options;
24
25
 
25
26
  return runBriefSynthesis({
26
- prompt: buildBriefSynthesisPrompt(snapshot, evidence, note),
27
+ prompt: buildBriefSynthesisPrompt(snapshot, serializedContext, note),
27
28
  model: model.model,
28
29
  modelRegistry,
29
30
  cwd,
@@ -34,7 +35,7 @@ export function synthesizeReviewBrief(
34
35
 
35
36
  export function buildBriefSynthesisPrompt(
36
37
  snapshot: ReviewSnapshot,
37
- evidence: HistoryEvidence[],
38
+ serializedContext: string,
38
39
  note?: string,
39
40
  ): string {
40
41
  const diffExcerpt = buildDiffExcerpt(snapshot.diffText);
@@ -65,18 +66,11 @@ export function buildBriefSynthesisPrompt(
65
66
  parts.push("", "## User note", note.trim());
66
67
  }
67
68
 
68
- parts.push("", "## Session evidence");
69
- if (evidence.length === 0) {
70
- parts.push(
71
- "- No strong session evidence was extracted. Derive a minimal brief from the snapshot only.",
72
- );
69
+ parts.push("", "## Serialized session context");
70
+ if (serializedContext.trim()) {
71
+ parts.push(serializedContext.trim());
73
72
  } else {
74
- parts.push(
75
- ...evidence.flatMap((item, index) => [
76
- `${index + 1}. [${item.kind}] ${item.reason}`,
77
- ` ${item.text}`,
78
- ]),
79
- );
73
+ parts.push("No session context was available. Derive the brief from the snapshot only.");
80
74
  }
81
75
 
82
76
  parts.push(
@@ -87,7 +81,9 @@ export function buildBriefSynthesisPrompt(
87
81
  "- intendedOutcome: what the session seems to be trying to achieve",
88
82
  "- constraints: invariants or requirements to preserve",
89
83
  "- focusAreas: what the reviewer should inspect carefully",
90
- "- riskyFiles: changed files that seem especially important or risky",
84
+ "- riskyFiles: changed files that seem especially important or risky.",
85
+ " Prioritize files touching auth, data handling, core logic, error handling, or public APIs.",
86
+ " Include only files from the changed-files list above.",
91
87
  "- unresolvedQuestions: ambiguities or concerns that remain unclear",
92
88
  );
93
89