feat: publish implementations with review findings

This commit is contained in:
2026-07-19 10:19:37 +02:00
parent 5c51323bff
commit 93bcbb5447
6 changed files with 157 additions and 9 deletions
@@ -135,12 +135,15 @@ export async function runImplementation(input: {
input.signal,
),
);
if (review.verdict === "accept") {
if (
review.verdict === "accept" ||
(iteration === maximumIterations && files.length)
) {
const pendingFiles = await changedFiles(
input.workspace,
options,
);
return acceptedResult({
return implementationResult({
input,
accepted,
prepared,
@@ -149,6 +152,7 @@ export async function runImplementation(input: {
diff,
summary: summary.summary,
rationale: review.rationale,
...(review.verdict === "revise" ? { review } : {}),
iteration,
session,
baseBranch: repository.default_branch,
@@ -179,7 +183,7 @@ export async function runImplementation(input: {
};
}
function acceptedResult(value: {
function implementationResult(value: {
input: { issueNumber: number };
accepted: NonNullable<ReturnType<typeof findAcceptedPlan>>;
prepared: {
@@ -192,6 +196,7 @@ function acceptedResult(value: {
diff: string;
summary: string;
rationale: string;
review?: ReturnType<typeof assertReviewDecision>;
iteration: number;
session: string;
baseBranch: string;
@@ -205,7 +210,11 @@ function acceptedResult(value: {
result: {
version: 1,
mode: "implement",
status: value.files.length ? "success" : "no-changes",
status: value.review
? "review-failed"
: value.files.length
? "success"
: "no-changes",
message: value.files.length
? value.rationale || "Implementation accepted"
: `${value.summary}\n\nReviewer: ${value.rationale}`,
@@ -222,6 +231,7 @@ function acceptedResult(value: {
pendingFiles: value.pendingFiles,
summary: value.summary,
iterations: value.iteration,
...(value.review ? { review: value.review } : {}),
},
},
};
@@ -10,6 +10,7 @@ import {
marker,
parseMarker,
protocolVersion,
type ReviewDecision,
sha256,
} from "../../../../core/contracts.js";
import {
@@ -93,6 +94,53 @@ export async function publishPullReview(
return { terminal: "succeeded", reviewId: published.id };
}
export async function publishRemainingImplementationReview(
context: PublicationContext,
input: {
job: Job;
pullRequestNumber: number;
commitSha: string;
review: ReviewDecision;
},
): Promise<number> {
const expected = {
v: protocolVersion,
kind: "review" as const,
issue: input.job.issueNumber,
mode: "implement" as const,
request: input.job.id,
pullRequest: input.pullRequestNumber,
};
const existing = (
await context.client.listPullReviews(input.pullRequestNumber)
).find((candidate) => {
if (
candidate.user.login.toLowerCase() !==
context.botLogin.toLowerCase()
)
return false;
const found = parseMarker(candidate.body, "review");
return (
found?.request === input.job.id &&
found.pullRequest === input.pullRequestNumber
);
});
if (existing) return existing.id;
const findings = input.review.findings.length
? input.review.findings.map((finding) => `- ${finding}`).join("\n")
: "The reviewer requested revision without itemized findings.";
const published = await context.client.createPullReview(
input.pullRequestNumber,
{
event: "COMMENT",
body: `${marker(expected)}\n${input.review.rationale}\n\n## Remaining issues\n\n${findings}`,
commit_id: input.commitSha,
comments: [],
},
);
return published.id;
}
async function publishStatus(
context: PublicationContext,
job: Job,
@@ -27,6 +27,7 @@ import {
type PublicationOutcome,
upsertJobStatus,
} from "../status.js";
import { publishRemainingImplementationReview } from "./conversations/review.js";
export async function publishImplementation(
context: PublicationContext,
@@ -39,6 +40,13 @@ export async function publishImplementation(
throw new Error(
"Successful implementation job has no workspace result",
);
const remainingReview =
result.status === "review-failed" ? implementation.review : undefined;
if (
result.status === "review-failed" &&
remainingReview?.verdict !== "revise"
)
throw new Error("Review-failed implementation has no final review");
const [issue, comments, currentBase] = await Promise.all([
context.client.getIssue(job.issueNumber),
context.client.getComments(job.issueNumber),
@@ -109,12 +117,23 @@ export async function publishImplementation(
issue.title,
);
await context.client.addLabelIfPresent(pull.number, generatedLabel);
if (remainingReview)
await publishRemainingImplementationReview(context, {
job,
pullRequestNumber: pull.number,
commitSha,
review: remainingReview,
});
await upsertJobStatus(
context.client,
context.botLogin,
job,
"Agent implementation ready",
`[Pull request #${pull.number}](${pull.html_url}) was created or updated.`,
remainingReview
? "Agent implementation published with review findings"
: "Agent implementation ready",
remainingReview
? `[Pull request #${pull.number}](${pull.html_url}) was created or updated with the remaining issues posted as a review.`
: `[Pull request #${pull.number}](${pull.html_url}) was created or updated.`,
);
return { terminal: "succeeded", commitSha, pullRequestNumber: pull.number };
}
+2 -1
View File
@@ -69,6 +69,7 @@ export interface ImplementationData {
pendingFiles: string[];
summary: string;
iterations: number;
review?: ReviewDecision;
pullRequestNumber?: number;
response?: string;
feedback?: FeedbackReference[];
@@ -77,7 +78,7 @@ export interface ImplementationData {
export interface Result {
version: number;
mode: Mode;
status: "success" | "no-changes" | "failed";
status: "success" | "no-changes" | "review-failed" | "failed";
message: string;
plan?: PlanData;
implementation?: ImplementationData;
+71 -1
View File
@@ -18,7 +18,10 @@ import type {
GiteaPullRequest,
GiteaPullReview,
} from "../../adapters/gitea/types.js";
import { publishPullReview } from "../../application/publication/handlers/conversations/review.js";
import {
publishPullReview,
publishRemainingImplementationReview,
} from "../../application/publication/handlers/conversations/review.js";
import type { PublicationContext } from "../../application/publication/status.js";
import { marker, protocolVersion, sha256 } from "../../core/contracts.js";
@@ -135,6 +138,73 @@ test("cleans a partial pending review before publishing inline findings", async
}
});
test("publishes remaining implementation findings once and exposes them as fix feedback", async () => {
const reviews: GiteaPullReview[] = [];
const created: CreatePullReviewInput[] = [];
const client = {
listPullReviews: async () => reviews,
createPullReview: async (
_pull: number,
input: CreatePullReviewInput,
) => {
created.push(input);
const review = pullReview(
80,
"agent",
"COMMENT",
input.body,
input.commit_id,
);
reviews.push(review);
return review;
},
} as unknown as GiteaClient;
const job = {
id: "implementation-job",
issueNumber: 2,
} as Job;
const input = {
job,
pullRequestNumber: 7,
commitSha: "c".repeat(40),
review: {
verdict: "revise" as const,
findings: ["Add a regression test.", "Handle the error path."],
rationale: "Two blocking issues remain.",
},
};
const first = await publishRemainingImplementationReview(
publicationContext(client),
input,
);
const second = await publishRemainingImplementationReview(
publicationContext(client),
input,
);
assert.equal(first, 80);
assert.equal(second, 80);
assert.equal(created.length, 1);
assert.equal(created[0]?.event, "COMMENT");
assert.equal(created[0]?.commit_id, input.commitSha);
assert.deepEqual(created[0]?.comments, []);
assert.match(created[0]?.body || "", /## Remaining issues/);
assert.match(created[0]?.body || "", /Add a regression test\./);
assert.match(created[0]?.body || "", /Handle the error path\./);
const feedback = await collectPullRequestFeedback(
{
getComments: async () => [],
listPullReviews: async () => reviews,
listPullReviewComments: async () => [],
},
7,
"agent",
);
assert.equal(feedback.length, 1);
assert.equal(feedback[0]?.kind, "review-summary");
assert.match(feedback[0]?.body || "", /Two blocking issues remain\./);
});
function reviewJob(
workspace: string,
base: string,