Verdict is the first line
Check / deno (pull_request) Successful in 41s

This commit is contained in:
2026-09-13 23:56:05 +09:00
parent 81e75297a4
commit 242717a141
2 changed files with 40 additions and 41 deletions
+29 -32
View File
@@ -52,29 +52,21 @@ async function postComment(body: string): Promise<void> {
});
}
// A review must read at a glance: when the agent left more than VISIBLE_BYTES
// outside <details> blocks, the whole review folds under its verdict line.
const VISIBLE_BYTES = 512;
function fold(body: string): string {
const visible = body.replace(/<details>[\s\S]*?<\/details>/g, "");
if (new TextEncoder().encode(visible).length <= VISIBLE_BYTES) return body;
const verdict = body.match(/\*\*Ready to merge\?\*\*.*/)?.[0] ?? "Review";
return `${verdict}\n\n<details>\n<summary>Details</summary>\n\n${body}\n\n</details>`;
}
// A pull request event is a review request, so the response becomes a review.
// Its first line is the verdict; anything unexpected only comments, never
// approves.
const VERDICTS: Record<string, string> = {
Yes: "APPROVED",
No: "REQUEST_CHANGES",
"With fixes": "REQUEST_CHANGES",
};
// A pull request event is a review request, so the response becomes a review:
// changes are requested when the agent asked @bot to fix something, a failed
// run only comments, and anything else approves.
async function postResult(body: string): Promise<void> {
if (EVENT !== "pull_request") return postComment(body);
const event = body.includes("@bot")
? "REQUEST_CHANGES"
: body.startsWith("Bot failed:")
? "COMMENT"
: "APPROVED";
const [verdict] = body.split("\n", 1);
await gitea(REVIEWER_TOKEN, `repos/${REPO}/pulls/${INDEX}/reviews`, {
body: fold(stripAnsi(body)),
event,
body: stripAnsi(body),
event: VERDICTS[verdict.trim()] ?? "COMMENT",
});
}
@@ -132,10 +124,9 @@ async function renderPrompt(): Promise<string> {
async function runClaude(prompt: string): Promise<string> {
const token = Deno.env.get("BOT_TOKEN");
if (!token) {
await postComment(
throw new Error(
"Run `claude setup-token` locally and set the `bot-token` action input.",
);
Deno.exit(1);
}
await installSuperpowers("claude");
const claude = new Deno.Command("claude", {
@@ -154,7 +145,7 @@ async function runClaude(prompt: string): Promise<string> {
clearEnv: true,
stdout: "piped",
}).spawn();
let result = "Bot failed: no result";
let result: { result?: string; subtype: string } | undefined;
// Print events as they stream so the runner does not kill the job as a zombie.
const lines = claude.stdout
.pipeThrough(new TextDecoderStream())
@@ -166,12 +157,13 @@ async function runClaude(prompt: string): Promise<string> {
const text = part.thinking ?? part.text ?? part.name;
if (text) console.log(text);
}
if (event.type === "result") {
result = event.result ?? `Bot failed: ${event.subtype}`;
}
if (event.type === "result") result = event;
}
await claude.status;
return result;
if (result?.result === undefined) {
throw new Error(`claude ended with ${result?.subtype ?? "no result"}`);
}
return result.result;
}
// Posts the device code so a human can finish the login on the persisted home.
@@ -222,9 +214,14 @@ async function runCodex(prompt: string): Promise<string> {
return await Deno.readTextFile(file);
}
await configureGitAuthor();
const prompt = await renderPrompt();
const result = env("BOT_TYPE") === "claude"
? await runClaude(prompt)
: await runCodex(prompt);
await postResult(result);
try {
await configureGitAuthor();
const prompt = await renderPrompt();
const result = env("BOT_TYPE") === "claude"
? await runClaude(prompt)
: await runCodex(prompt);
await postResult(result);
} catch (error) {
await postComment(`Bot failed: ${error}`);
throw error;
}