From d52596aad189d01073e6bde71e6578a94e94bd81 Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:10:13 +0800 Subject: [PATCH] feat: run robot review via pi harness --- README.md | 9 ++- src/index.ts | 164 +++++++++++++++++++++++++++++++-------------------- 2 files changed, 103 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 1a567ef..232845b 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ After calling this, the task shows `👀` and is only completable via `/lgtm { - return new Promise((resolve, reject) => { - const child = spawn("bash", ["-lc", command], { stdio: ["ignore", "pipe", "pipe"] }); - const stdoutChunks: Buffer[] = []; - const stderrChunks: Buffer[] = []; - child.stdout.on("data", (data) => stdoutChunks.push(data)); - child.stderr.on("data", (data) => stderrChunks.push(data)); - child.on("error", reject); - const onAbort = () => child.kill(); - signal?.addEventListener("abort", onAbort, { once: true }); - child.on("close", (exitCode) => { - signal?.removeEventListener("abort", onAbort); - if (signal?.aborted) { - reject(new Error("aborted")); - return; - } - resolve({ - stdout: Buffer.concat(stdoutChunks).toString("utf-8"), - stderr: Buffer.concat(stderrChunks).toString("utf-8"), - exitCode, - }); - }); - }); +function getPiInvocation(args: string[]): { command: string; args: string[] } { + const currentScript = process.argv[1]; + if (currentScript) { + return { command: process.execPath, args: [currentScript, ...args] }; + } + return { command: "pi", args }; } function extractRobotReviewJson(output: string): Record { @@ -128,11 +107,69 @@ async function runAutomaticRobotReview( task: any, signal?: AbortSignal, ): Promise<{ review: Omit; command: string }> { - const reviewerCommand = process.env.PI_LGTM_ROBOT_REVIEW_CMD?.trim() - || "acpx --approve-reads --non-interactive-permissions deny opencode exec"; const prompt = buildRobotReviewPrompt(task); - const command = `${reviewerCommand} ${shellQuote(prompt)}`; - const result = await runShellCommand(command, signal); + const args = ["--mode", "json", "-p", "--no-session"]; + const reviewerModel = process.env.PI_LGTM_ROBOT_REVIEW_MODEL?.trim(); + if (reviewerModel) args.push("--model", reviewerModel); + args.push(prompt); + const invocation = getPiInvocation(args); + const commandLabel = `${invocation.command} ${args.slice(0, reviewerModel ? 6 : 4).join(" ")}`; + const result = await new Promise((resolve, reject) => { + const child = spawn(invocation.command, invocation.args, { shell: false, stdio: ["ignore", "pipe", "pipe"] }); + const stdoutChunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; + let buffer = ""; + let finalAssistantText = ""; + child.stdout.on("data", (data) => { + stdoutChunks.push(data); + buffer += data.toString("utf-8"); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + for (const line of lines) { + if (!line.trim()) continue; + try { + const event = JSON.parse(line) as any; + if (event.type === "message_end" && event.message?.role === "assistant") { + const text = Array.isArray(event.message.content) + ? event.message.content.find((part: any) => part.type === "text")?.text + : undefined; + if (typeof text === "string") finalAssistantText = text; + } + } catch { + // ignore malformed line noise + } + } + }); + child.stderr.on("data", (data) => stderrChunks.push(data)); + child.on("error", reject); + const onAbort = () => child.kill(); + signal?.addEventListener("abort", onAbort, { once: true }); + child.on("close", (exitCode) => { + signal?.removeEventListener("abort", onAbort); + if (signal?.aborted) { + reject(new Error("aborted")); + return; + } + if (buffer.trim()) { + try { + const event = JSON.parse(buffer) as any; + if (event.type === "message_end" && event.message?.role === "assistant") { + const text = Array.isArray(event.message.content) + ? event.message.content.find((part: any) => part.type === "text")?.text + : undefined; + if (typeof text === "string") finalAssistantText = text; + } + } catch { + // ignore malformed trailing line + } + } + resolve({ + stdout: finalAssistantText || Buffer.concat(stdoutChunks).toString("utf-8"), + stderr: Buffer.concat(stderrChunks).toString("utf-8"), + exitCode, + }); + }); + }); if (result.exitCode !== 0) { throw new Error(`Robot reviewer failed (${result.exitCode ?? "?"}): ${(result.stderr || result.stdout).trim()}`); } @@ -143,9 +180,9 @@ async function runAutomaticRobotReview( ? parsed.missing_evidence.filter((item): item is string => typeof item === "string") : []; return { - command: reviewerCommand, + command: commandLabel, review: { - reviewer: typeof parsed.reviewer === "string" ? parsed.reviewer : reviewerCommand, + reviewer: typeof parsed.reviewer === "string" ? parsed.reviewer : commandLabel, scope: typeof parsed.scope === "string" ? parsed.scope : "task evidence package", observations, blind_spots: typeof parsed.blind_spots === "string" ? parsed.blind_spots : "not stated", @@ -502,7 +539,6 @@ After this, task enters pending sign-off state — only completable via /lgtm `- ${o}`).join("\n")}`; - if (review.missing_evidence.length > 0) { - robotReviewNote += `\nMissing evidence:\n${review.missing_evidence.map(item => `- ${item}`).join("\n")}`; - } - if (!(review.evidence_complete && review.evidence_convincing)) { - robotReviewNote += `\nResult: human sign-off has been held back until the evidence is strengthened and reviewed again.`; - } - } catch (err: any) { - robotReviewNote = - `\n\n### Automatic robot review\n` + - `Reviewer failed: ${err.message}\n` + - `Task remains pending human sign-off; rerun with stronger evidence or call \`robot_review_run\` after fixing reviewer setup.`; + const refreshedTask = store.get(params.taskId); + if (!refreshedTask) return textResult(`Task #${params.taskId} not found after evidence update`); + try { + const { review, command } = await runAutomaticRobotReview(refreshedTask, signal); + store.update(params.taskId, { + pending_approval: review.accepted, + metadata: appendRobotReviewMetadata(refreshedTask, review), + }); + robotReviewNote = + `\n\n### Automatic robot review\n` + + `Reviewer: ${command}\n` + + `Accepted: ${review.accepted ? "yes" : "no"}\n` + + `Evidence complete: ${review.evidence_complete ? "yes" : "no"}\n` + + `Evidence convincing: ${review.evidence_convincing ? "yes" : "no"}\n` + + `${review.observations.map(o => `- ${o}`).join("\n")}`; + if (review.missing_evidence.length > 0) { + robotReviewNote += `\nMissing evidence:\n${review.missing_evidence.map(item => `- ${item}`).join("\n")}`; } + if (!review.accepted) { + robotReviewNote += `\nResult: human sign-off has been held back until the evidence is strengthened and reviewed again.`; + } + } catch (err: any) { + store.update(params.taskId, { pending_approval: false }); + robotReviewNote = + `\n\n### Automatic robot review\n` + + `Reviewer failed: ${err.message}\n` + + `Human sign-off is blocked until the reviewer stage succeeds.`; } widget.update(); @@ -574,7 +609,7 @@ After this, task enters pending sign-off state — only completable via /lgtm