feat: add conversational agent reviews

This commit is contained in:
2026-07-18 23:39:54 +02:00
parent 595005b1cf
commit 2a6a8f73dd
50 changed files with 4523 additions and 587 deletions
+45 -82
View File
@@ -1,11 +1,36 @@
import { createHash } from "node:crypto";
import type {
FeedbackReference,
PlanDiscussionData,
PlanReviewData,
PullReviewData,
} from "./features/contracts.js";
export type {
FeedbackReference,
ImplementationFixReply,
PlanConversationReply,
PlanDiscussionData,
PlanReviewData,
PullReviewData,
PullReviewFinding,
PullReviewReply,
} from "./features/contracts.js";
export {
assertImplementationFixReply,
assertImplementationSummary,
assertPlanConversationReply,
assertPlanDraft,
assertPullReviewReply,
assertReviewDecision,
} from "./features/validation.js";
export const protocolVersion = 1;
export const planLabel = "agent:plan";
export const implementLabel = "agent:implement";
export const generatedLabel = "agent:generated";
export const blockedLabel = "agent:blocked";
export const planReadyLabel = "agent:plan-ready";
export const reviewFixLabel = "agent:fix-review";
export function log(
level: string,
@@ -41,8 +66,12 @@ export interface ImplementationData {
gitSafetyDigest: string;
diffDigest: string;
changedFiles: string[];
pendingFiles: string[];
summary: string;
iterations: number;
pullRequestNumber?: number;
response?: string;
feedback?: FeedbackReference[];
}
export interface Result {
@@ -52,11 +81,19 @@ export interface Result {
message: string;
plan?: PlanData;
implementation?: ImplementationData;
discussion?: PlanDiscussionData;
review?: PlanReviewData | PullReviewData;
}
export interface Marker {
v: number;
kind: "status" | "plan" | "implementation" | "pull-request";
kind:
| "status"
| "plan"
| "implementation"
| "pull-request"
| "conversation"
| "review";
issue: number;
mode?: Mode;
request?: string;
@@ -65,6 +102,7 @@ export interface Marker {
baseSha?: string;
planDigest?: string;
branch?: string;
pullRequest?: number;
}
export interface IssueSnapshot {
@@ -131,9 +169,12 @@ export function parseMarker(
}
}
export function statusMarker(issue: number, mode: Mode): Marker {
return { v: protocolVersion, kind: "status", issue, mode };
}
export const statusMarker = (issue: number, mode: Mode): Marker => ({
v: protocolVersion,
kind: "status",
issue,
mode,
});
export function formatError(error: unknown): string {
if (error instanceof Error) {
@@ -145,81 +186,3 @@ export function formatError(error: unknown): string {
}
return String(error);
}
export function assertPlanDraft(value: unknown): PlanDraft {
const draft = value as Partial<PlanDraft>;
if (
!draft ||
typeof draft.planMarkdown !== "string" ||
draft.planMarkdown.trim().length < 40
) {
throw new Error("Planner returned an invalid or empty plan");
}
if (draft.planMarkdown.length > 200_000)
throw new Error("Planner returned an oversized plan");
if (typeof draft.summary !== "string" || !draft.summary.trim()) {
throw new Error("Planner returned no summary");
}
if (draft.summary.length > 20_000)
throw new Error("Planner returned an oversized summary");
return {
planMarkdown: draft.planMarkdown.trim(),
summary: draft.summary.trim(),
};
}
export function assertReviewDecision(value: unknown): ReviewDecision {
const review = value as Partial<ReviewDecision>;
if (review?.verdict !== "accept" && review?.verdict !== "revise") {
throw new Error("Reviewer returned an invalid verdict");
}
if (
!Array.isArray(review.findings) ||
!review.findings.every((item) => typeof item === "string")
) {
throw new Error("Reviewer returned invalid findings");
}
if (
review.findings.length > 100 ||
review.findings.some((item) => item.length > 10_000)
) {
throw new Error("Reviewer returned oversized findings");
}
if (typeof review.rationale !== "string")
throw new Error("Reviewer returned no rationale");
if (review.rationale.length > 20_000)
throw new Error("Reviewer returned an oversized rationale");
return {
verdict: review.verdict,
findings: review.findings.map((item) => item.trim()).filter(Boolean),
rationale: review.rationale.trim(),
};
}
export function assertImplementationSummary(
value: unknown,
): ImplementationSummary {
const summary = value as Partial<ImplementationSummary>;
if (
!summary ||
typeof summary.summary !== "string" ||
!summary.summary.trim()
) {
throw new Error("Implementation agent returned no summary");
}
if (summary.summary.length > 20_000)
throw new Error("Implementation agent returned an oversized summary");
if (
!Array.isArray(summary.files) ||
!summary.files.every((item) => typeof item === "string")
) {
throw new Error("Implementation agent returned an invalid file list");
}
if (
summary.files.length > 100 ||
summary.files.some((item) => item.length > 1_000)
) {
throw new Error("Implementation agent returned an oversized file list");
}
return { summary: summary.summary.trim(), files: summary.files };
}