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
@@ -0,0 +1,110 @@
import type { Job } from "../../../../adapters/database/store.js";
import {
parseUnifiedDiff,
renderPullReviewComments,
validateStructuredFindings,
} from "../../../../adapters/git/diff.js";
import { workspaceDiff } from "../../../../adapters/git/repository/changes.js";
import { headSha } from "../../../../adapters/git/repository/checkout.js";
import {
marker,
parseMarker,
protocolVersion,
sha256,
} from "../../../../core/contracts.js";
import {
type PublicationContext,
type PublicationOutcome,
upsertJobStatus,
} from "../../status.js";
export async function publishPullReview(
context: PublicationContext,
job: Job,
): Promise<PublicationOutcome> {
const review = job.result?.review;
const workspace = job.workspace;
if (review?.kind !== "pull" || !workspace)
throw new Error("Pull review result is incomplete");
const pull = await context.client.getPullRequest(job.targetNumber);
if (
pull.state !== "open" ||
pull.number !== review.pullRequestNumber ||
pull.head.sha !== review.headSha ||
pull.merge_base !== review.mergeBaseSha
)
throw new Error(
"Pull request changed while its review was being published",
);
const options = context.signal ? { signal: context.signal } : {};
if ((await headSha(workspace, options)) !== review.headSha)
throw new Error(
"Review workspace does not match the pull request head",
);
const diff = await workspaceDiff(workspace, review.mergeBaseSha, options);
if (sha256(diff) !== review.diffDigest)
throw new Error("Pull-request diff changed after review");
const parsed = parseUnifiedDiff(diff);
const findings = validateStructuredFindings(review.findings, parsed);
const reviewMarker = {
v: protocolVersion,
kind: "review" as const,
issue: job.issueNumber,
mode: "implement" as const,
request: job.id,
pullRequest: job.targetNumber,
};
const reviews = await context.client.listPullReviews(job.targetNumber);
const existing = reviews.find((candidate) => {
if (
candidate.user.login.toLowerCase() !==
context.botLogin.toLowerCase()
)
return false;
const found = parseMarker(candidate.body, "review");
return (
found?.request === job.id && found.pullRequest === job.targetNumber
);
});
const pending = reviews.filter(
(candidate) =>
candidate.user.login.toLowerCase() ===
context.botLogin.toLowerCase() &&
candidate.state.toUpperCase() === "PENDING" &&
(!candidate.commit_id || candidate.commit_id === review.headSha),
);
for (const candidate of pending)
await context.client.deletePullReview(job.targetNumber, candidate.id);
if (existing && existing.state.toUpperCase() !== "PENDING") {
await publishStatus(context, job, findings.length);
return { terminal: "succeeded", reviewId: existing.id };
}
const general = review.generalFindings.length
? `\n\n## General findings\n\n${review.generalFindings.map((item) => `- ${item}`).join("\n")}`
: "";
const body = `${marker(reviewMarker)}\n${review.summary}${general}`;
const published = await context.client.createPullReview(job.targetNumber, {
event: "COMMENT",
body,
commit_id: review.headSha,
comments: renderPullReviewComments(findings),
});
await publishStatus(context, job, findings.length);
return { terminal: "succeeded", reviewId: published.id };
}
async function publishStatus(
context: PublicationContext,
job: Job,
findingCount: number,
): Promise<void> {
await upsertJobStatus(
context.client,
context.botLogin,
job,
"Agent pull request review completed",
findingCount
? `${findingCount} inline finding(s) were published.`
: "No blocking inline findings were reported.",
);
}