fix(gate): reviewer_confirm requires explicit approve; free-text is actionable, not accidental approve

This commit is contained in:
2026-07-02 18:30:14 +02:00
parent 2633b46199
commit 644634d492
2 changed files with 56 additions and 8 deletions
+29 -8
View File
@@ -253,6 +253,20 @@ export function resolveSelectOutcome(opts, resp) {
return { kind: "choice", option, choice };
}
// Classifies a reviewer_confirm response. Advance happens ONLY on an explicit
// "approve" choice; a bare/unknown response resolves to {kind:"unknown"} and is
// re-presented (never an accidental approve). freetext is actionable feedback,
// distinct from an explicit "reject".
export function resolveConfirmOutcome(resp) {
if (resp?.control === "freetext") return { kind: "freetext", text: resp.text };
if (resp?.control === "back") return { kind: "back" };
if (resp?.control === "exit") return { kind: "exit" };
const choice = selectedChoice(resp);
if (choice === "approve") return { kind: "approve" };
if (choice === "reject") return { kind: "reject" };
return { kind: "unknown", choice };
}
export async function emitAndWait(ctx, descriptor) {
for (;;) {
const value = await ctx.ui.input(JSON.stringify(descriptor), "");
@@ -582,17 +596,24 @@ export default function (pi) {
artifact,
action: { kind: "approve_reject", prompt: "Approvi o rifiuti?" },
});
const resp = await emitAndWait(ctx, widget);
if (resp.control === "freetext" || selectedChoice(resp) === "reject") {
return textResult(
`Rifiutato${resp.text ? ` (motivo: ${resp.text})` : ""}: rivedi e riprova.`,
);
let outcome;
for (;;) {
const resp = await emitAndWait(ctx, widget);
outcome = resolveConfirmOutcome(resp);
if (outcome.kind !== "unknown") break;
await ctx.ui.notify("Scegli «Conferma e prosegui» o «Rifiuta».", "warning");
}
if (resp.control === "back")
if (outcome.kind === "freetext")
return textResult(
`Altro (reviewer): ${outcome.text}. Valuta e agisci, poi ri-presenta il gate.`,
);
if (outcome.kind === "reject")
return textResult("Rifiutato: rivedi e riprova.");
if (outcome.kind === "back")
return textResult("Il reviewer vuole tornare indietro.");
if (resp.control === "exit")
if (outcome.kind === "exit")
return textResult("Il reviewer vuole uscire.");
// approved -> execute the privileged action via the CLI.
// outcome.kind === "approve" -> execute the privileged action via the CLI.
if (kind === "phase") {
// Explicit human approval: advance unconditionally except for unmet
// prerequisites. Plain `phase advance` (no --auto) enforces advance_problems