Fail-closed verdict (#3)

Fail-closed verdict (#3)

Co-authored-by: Danny Kim <temeddix@gmail.com>
Co-committed-by: Danny Kim <temeddix@gmail.com>
This commit was merged in pull request #3.
This commit is contained in:
2026-09-13 14:57:01 +00:00
committed by temeddix
parent b8d0093ef2
commit f0506adfea
2 changed files with 42 additions and 25 deletions
+29 -22
View File
@@ -52,19 +52,21 @@ async function postComment(body: string): Promise<void> {
});
}
// 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.
// 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",
};
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: stripAnsi(body),
event,
event: VERDICTS[verdict.trim()] ?? "COMMENT",
});
}
@@ -122,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", {
@@ -144,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())
@@ -156,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.
@@ -212,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;
}