/**
* Critique — reviewer prompt and model call.
*
* The critique model runs with no tools: it questions the work step purely from
* serialized context, regardless of domain. Its Markdown output is injected
* back into the working model as advisory feedback.
*/
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
import { type Message, type Model, uuidv7 } from "@earendil-works/pi-ai";
const MAX_REVIEW_CHARS = 16_000;
export const REVIEWER_SYSTEM_PROMPT = [
"Independent adversarial reviewer. No tools; judge only given work step.",
"Any domain: code, writing, planning, research, data, ops, etc.",
"Check: correctness/truth, logic, assumptions, completeness, risks, clarity, efficiency. For code also robustness/security/maintainability.",
"Concrete refs: files/lines/cmds/tool results when present. No invented issues. If OK, say OK. If truncated, note limits.",
"Markdown exactly:",
"## Verdict",
"APPROVED | APPROVED_WITH_SUGGESTIONS | CHANGES_RECOMMENDED",
"## Issues",
"- [severity: critical|major|minor] description",
"## Suggestions",
"- action",
"## Summary",
"2-4 sentences.",
].join("\n");
export function buildReviewPrompt(workText: string, focusNote: string): string {
const sections: string[] = [];
sections.push("Review latest project work.");
sections.push("");
sections.push("");
sections.push(workText);
sections.push("");
if (focusNote) {
sections.push("");
sections.push("");
sections.push(focusNote);
sections.push("");
sections.push("Prioritize focus; still review all.");
}
return sections.join("\n");
}
/**
* Run the critique model. Returns the review text, or null when aborted.
*/
export async function runCritique(
ctx: ExtensionContext,
model: Model,
workText: string,
focusNote: string,
signal?: AbortSignal,
): Promise {
const userMessage: Message = {
role: "user",
content: [{ type: "text", text: buildReviewPrompt(workText, focusNote) }],
timestamp: Date.now(),
};
const response = await ctx.modelRegistry.complete(
model,
{ systemPrompt: REVIEWER_SYSTEM_PROMPT, messages: [userMessage] },
{ signal, cacheRetention: "none", sessionId: uuidv7() },
);
if (response.stopReason === "aborted") return null;
if (response.stopReason === "error") {
throw new Error(`Critique model ${model.provider}/${model.id} failed: ${response.errorMessage ?? "unknown error"}`);
}
const review = response.content
.filter((block): block is { type: "text"; text: string } => block.type === "text")
.map((block) => block.text)
.join("\n")
.trim();
// Cap the review so the injected message stays reasonable.
return review.length > MAX_REVIEW_CHARS
? `${review.slice(0, MAX_REVIEW_CHARS)}… [review truncated]`
: review;
}
/**
* Message injected back into the main agent. Shaped as advisory feedback:
* the working model is the final judge and may accept, partially accept, or
* reject the critique — the feedback is not a mandatory instruction.
*/
export function buildInjectedMessage(critiqueModel: string, review: string): string {
return [
"[Critique — advisory]",
"",
`Reviewer: \`${critiqueModel}\`. Advice only: apply useful points; briefly reject bad ones.`,
"",
"---",
"",
review,
].join("\n");
}