fix(executor): bypass summary-incomplete refusal for PREMISE STALE: summaries

Address the code-review finding on the prior commit: a natural premise-stale
summary like "PREMISE STALE: the task has no remaining work — implementation
is already done on HEAD" matches /\b(incomplete|not implemented|not done|
not finished)\b/i with 'the task' inside the 40-char first-person window,
refusing fn_task_done with summary-claims-incomplete and deadlocking the
escape hatch.

When summary starts (case-insensitive) with PREMISE STALE:, skip the
dissent-pattern and scoped-incomplete summary checks. Pending-code-review
and bulk-step-completion guards still apply unchanged.

Add executor-task-done-premise-stale.test.ts covering: the deadlock case
now passes; dissent phrasing in a sentinel summary is allowed; case-
insensitive sentinel; sentinel must be at the start (mid-summary doesn't
bypass); REVISE verdict still blocks even with the sentinel.
This commit is contained in:
gsxdsm
2026-05-22 22:34:32 -07:00
parent 8a3afcf9d4
commit 838002491d
3 changed files with 103 additions and 1 deletions

View File

@@ -0,0 +1,85 @@
import { describe, expect, it } from "vitest";
import { evaluateTaskDoneRefusal } from "../executor.js";
function createTask(stepStatuses: Array<"done" | "skipped" | "pending" | "in-progress">) {
return {
id: "FN-PREMISE-STALE",
title: "Premise stale",
description: "",
column: "in-progress",
dependencies: [],
steps: stepStatuses.map((status, index) => ({ name: `Step ${index + 1}`, status })),
currentStep: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as any;
}
describe("preflight PREMISE STALE: escape hatch", () => {
it("allows fn_task_done when summary starts with PREMISE STALE: even if it contains 'done' near 'the task'", () => {
// Without the bypass, the scoped-incomplete regex matches 'done' and the
// 40-char window contains 'the task' → would refuse with
// summary-claims-incomplete. The bypass must let this through.
const task = createTask(["done", "skipped", "skipped", "skipped", "skipped"]);
const result = evaluateTaskDoneRefusal(
task,
{ summary: "PREMISE STALE: the task has no remaining work — implementation is already done on HEAD" },
new Map(),
);
expect(result).toEqual({ ok: true });
});
it("allows fn_task_done when summary starts with PREMISE STALE: and contains 'I'm blocked' style dissent phrasing", () => {
// Natural premise-stale phrasing may accidentally include a dissent-pattern
// word ("blocked from", "to unblock", "requires follow-up"). The bypass
// must not refuse on the dissent regex when the sentinel is present.
const task = createTask(["done", "skipped", "skipped"]);
const result = evaluateTaskDoneRefusal(
task,
{ summary: "PREMISE STALE: targeted reproduction passes on HEAD; nothing to unblock and no further work required" },
new Map(),
);
expect(result).toEqual({ ok: true });
});
it("is case-insensitive on the sentinel", () => {
const task = createTask(["done", "skipped"]);
const result = evaluateTaskDoneRefusal(
task,
{ summary: "premise stale: this task is not done because main already shipped it" },
new Map(),
);
expect(result).toEqual({ ok: true });
});
it("does NOT bypass when sentinel appears later in the summary (must be at start)", () => {
// Defends against agents tacking the sentinel into the middle to dodge a
// genuine incomplete-work refusal.
const task = createTask(["done", "pending"]);
const result = evaluateTaskDoneRefusal(
task,
{ summary: "The task is not done yet, but PREMISE STALE: I think it's stale anyway" },
new Map(),
);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.refusalClass).toBe("summary-claims-incomplete");
}
});
it("still enforces pending-code-review-revise even with the sentinel", () => {
// The bypass only relaxes the summary-text checks. A genuine REVISE verdict
// on an in-progress step must still block fn_task_done.
const task = createTask(["done", "in-progress"]);
const verdicts = new Map<number, "REVISE">([[1, "REVISE"]]);
const result = evaluateTaskDoneRefusal(
task,
{ summary: "PREMISE STALE: already done on HEAD" },
verdicts as any,
);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.refusalClass).toBe("pending-code-review-revise");
}
});
});

View File

@@ -310,7 +310,15 @@ export function evaluateTaskDoneRefusal(
}
const summary = params.summary?.trim();
if (summary) {
// Preflight escape hatch: when the agent's preflight finds PROMPT.md is out
// of sync with HEAD (work already done on the base), it marks remaining
// steps `skipped` and calls fn_task_done with a `PREMISE STALE:` summary.
// Skip the summary-text refusals (dissent + scoped-incomplete) for this
// sentinel so a natural premise-stale explanation like "...the work is
// already done on HEAD" cannot deadlock the executor. The pending-review
// and bulk-step-completion guards above/below still apply.
const isPremiseStale = !!summary && /^premise stale:/i.test(summary);
if (summary && !isPremiseStale) {
const dissentMatch = DISSENT_PATTERNS.find((pattern) => pattern.test(summary));
if (dissentMatch) {
const matchText = summary.match(dissentMatch)?.[0] ?? dissentMatch.source;