feat(FN-4851): merge fusion/fn-4851

This commit is contained in:
gsxdsm
2026-05-17 00:38:07 -07:00
parent 384a6abe96
commit 3ca3b5e471
7 changed files with 548 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Guard `fn_task_done` against agent-dissent summaries, bulk auto-marking of unreviewed pending steps, and pending REVISE verdicts. These refusals share the existing requeue budget and escalate tasks to in-review when retries are exhausted.

View File

@@ -543,6 +543,7 @@ When debugging agent execution issues (agents stuck on "starting"), check these
13. **`[wake-trigger-diagnostics] agent=<id> run=<id> triggerDetail=<wake-on-message*> source=<source> messageId=<id|none> from=<type:id|none> forced=<bool> createdAt=<iso|none> inboxUnreadCount=<n> wakeMessageStillUnread=<true|false|unknown> pendingRoomMessages=<n>`** — Correlates wake-on-message triggers with inbox snapshot state to diagnose empty-inbox false-positive wakes
14. **`[retry-burned] retry-burned { taskId, agentId, role, category, attempt, total, breakdown }`** — Unified retry-burn telemetry and retry-cap circuit-breaker context
15. **`Worktree init command failed (first test run will likely fail): ...` task log entries** — FN-4834: when `worktreeInitCommand` fails, diagnostic stderr is now written to the entry `outcome` (stdout/spawnError fallback), so dashboard logs preserve init failure details without rerunning.
16. **`[executor] FN-XXX: fn_task_done refused (<class>) — <reason>`** — FN-4851 refusal diagnostics for `summary-claims-incomplete`, `bulk-step-completion-without-review`, and `pending-code-review-revise`; all three consume the shared `taskDoneRetryCount` budget and escalate to `in-review` once retries are exhausted.
### Semaphore Resilience

View File

@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { evaluateTaskDoneRefusal } from "../executor.js";
function createTask(stepStatuses: Array<"done" | "skipped" | "pending" | "in-progress">) {
return {
id: "FN-4851",
title: "Bulk guard",
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("FN-4851 bulk-step-completion guard", () => {
it("refuses when 2+ steps would be auto-completed without full APPROVE evidence", () => {
const task = createTask(["done", "done", "pending", "pending", "pending", "pending", "pending"]);
const result = evaluateTaskDoneRefusal(task, { summary: "Implemented all requested work and verified." }, new Map());
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.refusalClass).toBe("bulk-step-completion-without-review");
}
});
it("allows a single pending step without review evidence", () => {
const task = createTask(["done", "done", "done", "done", "done", "done", "pending"]);
const result = evaluateTaskDoneRefusal(task, { summary: "All tasks complete." }, new Map());
expect(result).toEqual({ ok: true });
});
it("allows bulk completion when all pending steps are APPROVE", () => {
const task = createTask(["done", "done", "done", "done", "pending", "pending", "pending"]);
const verdicts = new Map<number, "APPROVE">([
[4, "APPROVE"],
[5, "APPROVE"],
[6, "APPROVE"],
]);
const result = evaluateTaskDoneRefusal(task, { summary: "All tasks complete." }, verdicts as any);
expect(result).toEqual({ ok: true });
});
it("refuses when even one pending step lacks APPROVE", () => {
const task = createTask(["done", "done", "done", "done", "done", "pending", "pending"]);
const verdicts = new Map<number, "APPROVE">([[5, "APPROVE"]]);
const result = evaluateTaskDoneRefusal(task, { summary: "All tasks complete." }, verdicts as any);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.refusalClass).toBe("bulk-step-completion-without-review");
}
});
});

View File

@@ -0,0 +1,104 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import "./executor-test-helpers.js";
import { TaskExecutor } from "../executor.js";
import * as worktreePool from "../worktree-pool.js";
import { createMockStore, mockedCreateFnAgent, mockedExecSync, resetExecutorMocks } from "./executor-test-helpers.js";
function createTask(overrides: Record<string, unknown> = {}) {
return {
id: "FN-4851",
title: "Task done refusal test",
description: "",
column: "in-progress",
worktree: "/repo/.worktrees/swift-falcon",
branch: "fusion/fn-4851",
baseCommitSha: "abc123",
taskDoneRetryCount: 0,
dependencies: [],
steps: [{ name: "Implementation", status: "in-progress" as const }],
currentStep: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}
async function setup(overrides: Record<string, unknown> = {}) {
const store = createMockStore();
let task: any = createTask(overrides);
let doneTool: any;
store.getTask.mockImplementation(async () => ({ ...task, steps: task.steps.map((s: any) => ({ ...s })) }));
store.moveTask.mockImplementation(async (_id: string, column: string) => {
task = { ...task, column };
});
mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => {
doneTool = customTools.find((tool: any) => tool.name === "fn_task_done");
return { session: { prompt: vi.fn().mockResolvedValue(undefined), dispose: vi.fn() } } as any;
});
const executor = new TaskExecutor(store as any, "/repo");
await executor.execute(createTask() as any);
return { store, doneTool };
}
describe("FN-4851 dissent guard", () => {
beforeEach(() => {
resetExecutorMocks();
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
mockedExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/swift-falcon\n");
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/fn-4851\n");
if (cmd.includes("rev-list --count")) return Buffer.from("1\n");
if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n");
return Buffer.from("");
});
});
it("refuses summary that directly claims incompletion", async () => {
const { store, doneTool } = await setup();
const result = await doneTool.execute("id", { summary: "Task is not complete. I'm blocked from safely finishing this." });
expect(result.details.refusalClass).toBe("summary-claims-incomplete");
expect(result.content[0].text).toContain("fn_task_done refused (summary-claims-incomplete)");
expect(store.moveTask).toHaveBeenCalledWith("FN-4851", "todo", { preserveProgress: true });
expect(store.updateTask).toHaveBeenCalledWith("FN-4851", expect.objectContaining({ taskDoneRetryCount: 1 }));
});
it("refuses 'To unblock' summary", async () => {
const { doneTool } = await setup();
const result = await doneTool.execute("id", { summary: "To unblock, sync/land FN-4789 before I can finish." });
expect(result.details.refusalClass).toBe("summary-claims-incomplete");
});
it("refuses summary that says it needs another FN task", async () => {
const { doneTool } = await setup();
const result = await doneTool.execute("id", { summary: "This needs FN-1234 before completion." });
expect(result.details.refusalClass).toBe("summary-claims-incomplete");
});
it("allows bare 'incomplete' without first-person/task context", async () => {
const { doneTool } = await setup();
const result = await doneTool.execute("id", { summary: "Fixed incomplete dependency declaration in package.json" });
expect(result.details.refusalClass).toBeUndefined();
expect(result.content[0].text).toContain("Task marked complete");
});
it("does not trigger dissent guard for empty summary", async () => {
const { doneTool } = await setup();
const result = await doneTool.execute("id", {});
expect(result.details.refusalClass).toBeUndefined();
expect(result.content[0].text).toContain("Task marked complete");
});
});

View File

@@ -0,0 +1,100 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import "./executor-test-helpers.js";
import { TaskExecutor } from "../executor.js";
import { reviewStep } from "../reviewer.js";
import * as worktreePool from "../worktree-pool.js";
import { createMockStore, mockedCreateFnAgent, mockedExecSync, resetExecutorMocks } from "./executor-test-helpers.js";
function createTask(overrides: Record<string, unknown> = {}) {
return {
id: "FN-4851",
title: "REVISE guard",
description: "",
column: "in-progress",
worktree: "/repo/.worktrees/swift-falcon",
branch: "fusion/fn-4851",
baseCommitSha: "abc123",
taskDoneRetryCount: 0,
dependencies: [],
steps: [{ name: "Step 1", status: "in-progress" as const }],
currentStep: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}
async function setup(overrides: Record<string, unknown> = {}) {
const store = createMockStore();
let task: any = createTask(overrides);
let doneTool: any;
let reviewTool: any;
store.getTask.mockImplementation(async () => ({ ...task, steps: task.steps.map((s: any) => ({ ...s })) }));
store.moveTask.mockImplementation(async (_id: string, column: string) => {
task = { ...task, column };
});
mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => {
doneTool = customTools.find((tool: any) => tool.name === "fn_task_done");
reviewTool = customTools.find((tool: any) => tool.name === "fn_review_step");
return { session: { prompt: vi.fn().mockResolvedValue(undefined), dispose: vi.fn() } } as any;
});
const executor = new TaskExecutor(store as any, "/repo");
await executor.execute(createTask() as any);
return { store, doneTool, reviewTool };
}
describe("FN-4851 REVISE verdict task-done guard", () => {
beforeEach(() => {
resetExecutorMocks();
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
mockedExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/swift-falcon\n");
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/fn-4851\n");
if (cmd.includes("rev-list --count")) return Buffer.from("1\n");
if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n");
return Buffer.from("");
});
vi.mocked(reviewStep).mockResolvedValue({
verdict: "REVISE",
summary: "Needs fixes",
review: "Please fix issues",
} as any);
});
it("refuses fn_task_done when a pending step has REVISE verdict", async () => {
const { store, reviewTool, doneTool } = await setup();
await reviewTool.execute("rev", { step: 0, type: "code", step_name: "Step 1", baseline: "abc123" });
const result = await doneTool.execute("done", { summary: "Implemented all requested changes." });
expect(result.details.refusalClass).toBe("pending-code-review-revise");
expect(store.moveTask).toHaveBeenCalledWith("FN-4851", "todo", { preserveProgress: true });
});
it("escalates to in-review when retry budget is exhausted", async () => {
const { store, reviewTool, doneTool } = await setup({ taskDoneRetryCount: 3 });
await reviewTool.execute("rev", { step: 0, type: "code", step_name: "Step 1", baseline: "abc123" });
const result = await doneTool.execute("done", { summary: "Implemented all requested changes." });
expect(result.details.refusalClass).toBe("pending-code-review-revise");
expect(result.details.error).toContain("pending-code-review-revise");
expect(store.moveTask).toHaveBeenCalledWith("FN-4851", "in-review");
expect(store.updateTask).toHaveBeenCalledWith("FN-4851", expect.objectContaining({ status: "failed" }));
});
it("ignores REVISE verdict on already done or skipped steps", async () => {
const { doneTool } = await setup({
steps: [{ name: "Step 1", status: "done" }, { name: "Step 2", status: "skipped" }],
});
const result = await doneTool.execute("done", { summary: "Implemented all requested changes." });
expect(result.details.refusalClass).toBeUndefined();
expect(result.content[0].text).toContain("Task marked complete");
});
});

View File

@@ -0,0 +1,116 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import "../executor-test-helpers.js";
import { TaskExecutor } from "../../executor.js";
import { reviewStep } from "../../reviewer.js";
import * as worktreePool from "../../worktree-pool.js";
import { createMockStore, mockedCreateFnAgent, mockedExecSync, resetExecutorMocks } from "../executor-test-helpers.js";
function createTask(overrides: Record<string, unknown> = {}) {
return {
id: "FN-4851",
title: "Reliability ordering",
description: "",
column: "in-progress",
worktree: "/repo/.worktrees/swift-falcon",
branch: "fusion/fn-4851",
baseCommitSha: "abc123",
taskDoneRetryCount: 0,
dependencies: [],
steps: [{ name: "Step 1", status: "in-progress" as const }, { name: "Step 2", status: "pending" as const }],
currentStep: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}
async function setup(overrides: Record<string, unknown> = {}) {
const store = createMockStore();
let task: any = createTask(overrides);
let doneTool: any;
let reviewTool: any;
store.getTask.mockImplementation(async () => ({ ...task, steps: task.steps.map((s: any) => ({ ...s })) }));
store.updateTask.mockImplementation(async (_id: string, patch: any) => {
task = { ...task, ...patch };
return { ...task, steps: task.steps.map((s: any) => ({ ...s })) };
});
store.moveTask.mockImplementation(async (_id: string, column: string) => {
task = { ...task, column };
});
mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => {
doneTool = customTools.find((tool: any) => tool.name === "fn_task_done");
reviewTool = customTools.find((tool: any) => tool.name === "fn_review_step");
return { session: { prompt: vi.fn().mockResolvedValue(undefined), dispose: vi.fn() } } as any;
});
const executor = new TaskExecutor(store as any, "/repo");
await executor.execute(createTask() as any);
return { store, doneTool, reviewTool, getTask: () => task };
}
describe("FN-4851 reliability interactions: task-done refusals x invariant", () => {
beforeEach(() => {
resetExecutorMocks();
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
mockedExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/swift-falcon\n");
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/fn-4851\n");
if (cmd.includes("rev-list --count")) return Buffer.from("1\n");
if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n");
return Buffer.from("");
});
vi.mocked(reviewStep).mockResolvedValue({ verdict: "REVISE", summary: "needs work", review: "fix" } as any);
});
it("lets invariant refusal win over summary dissent", async () => {
const invariantSpy = vi.spyOn(TaskExecutor.prototype as any, "verifyWorktreeInvariants").mockResolvedValue({
ok: false,
reason: "wrong_branch",
observed: "main",
expected: "fusion/fn-4851",
});
const { doneTool } = await setup();
const result = await doneTool.execute("done", { summary: "Task is not complete and I am blocked." });
expect(result.content[0].text).toContain("fn_task_done refused: wrong_branch");
expect(result.details.refusalClass).toBeUndefined();
invariantSpy.mockRestore();
});
it("runs dissent refusal before scope-leak guard when invariants pass", async () => {
const invariantSpy = vi.spyOn(TaskExecutor.prototype as any, "verifyWorktreeInvariants").mockResolvedValue({ ok: true });
const scopeSpy = vi.spyOn(TaskExecutor.prototype as any, "evaluateTaskDoneScopeLeak");
const { doneTool } = await setup();
const result = await doneTool.execute("done", { summary: "To unblock, land FN-4789 first." });
expect(result.details.refusalClass).toBe("summary-claims-incomplete");
expect(scopeSpy).not.toHaveBeenCalled();
scopeSpy.mockRestore();
invariantSpy.mockRestore();
});
it("shares one retry budget across mixed refusal classes", async () => {
const { doneTool, reviewTool, store, getTask } = await setup({ steps: [{ name: "S1", status: "in-progress" }, { name: "S2", status: "pending" }] });
await doneTool.execute("1", { summary: "Task is not complete." });
expect(getTask().taskDoneRetryCount).toBe(1);
await doneTool.execute("2", { summary: "Completed implementation and tests." });
expect(getTask().taskDoneRetryCount).toBe(2);
getTask().steps = [{ name: "S1", status: "in-progress" }];
await reviewTool.execute("rev", { step: 0, type: "code", step_name: "S1", baseline: "abc" });
const third = await doneTool.execute("3", { summary: "Completed implementation and tests." });
expect(third.details.refusalClass).toBe("pending-code-review-revise");
expect(getTask().taskDoneRetryCount).toBe(3);
const fourth = await doneTool.execute("4", { summary: "Task is not complete." });
expect(fourth.details.refusalClass).toBe("pending-code-review-revise");
expect(store.moveTask).toHaveBeenCalledWith("FN-4851", "in-review");
});
});

View File

@@ -171,6 +171,108 @@ const COMPLETED_TASK_WATCHDOG_MS = 60_000;
/** How long to wait before retrying a workflow rerun handoff that never reached in-progress. */
const WORKFLOW_RERUN_WATCHDOG_MS = 15_000;
const TASK_DONE_REFUSAL_SUFFIX = "Either finish the work and resubmit, or do not call fn_task_done — exit the session and the engine will requeue.";
export const DISSENT_PATTERNS: RegExp[] = [
/\btask (is|was)(?: not|n['’]?t) complete\b/i,
/\b(?:i (?:could|can)(?:not|n['’]?t)|unable to|failed to) (?:complete|finish|implement)\b/i,
/\b(?:partially|not fully) (?:complete|implemented|done|finished)\b/i,
/\b(?:i['’]?m blocked|blocked from|blocking issue prevents)\b/i,
/\bto unblock\b/i,
/\b(?:needs|requires) (?:FN-\d+|further work|additional work|follow[- ]?up)\b/i,
];
type TaskDoneRefusalClass =
| "summary-claims-incomplete"
| "bulk-step-completion-without-review"
| "pending-code-review-revise";
type TaskDoneRefusalResult =
| { ok: true }
| {
ok: false;
refusalClass: TaskDoneRefusalClass;
message: string;
reason: string;
};
function formatTaskDoneRefusal(refusalClass: TaskDoneRefusalClass, reason: string): string {
return `fn_task_done refused (${refusalClass}): ${reason}. ${TASK_DONE_REFUSAL_SUFFIX}`;
}
export function evaluateTaskDoneRefusal(
task: Task,
params: { summary?: string },
codeReviewVerdicts: Map<number, ReviewVerdict>,
): TaskDoneRefusalResult {
const pendingSteps: number[] = [];
for (let stepIndex = 0; stepIndex < task.steps.length; stepIndex++) {
const step = task.steps[stepIndex];
if (!step || step.status === "done" || step.status === "skipped") {
continue;
}
pendingSteps.push(stepIndex);
if (codeReviewVerdicts.get(stepIndex) === "REVISE") {
const reason = `Step ${stepIndex + 1} (${step.name}) has a pending code review verdict of REVISE`;
return {
ok: false,
refusalClass: "pending-code-review-revise",
reason,
message: formatTaskDoneRefusal("pending-code-review-revise", reason),
};
}
}
const summary = params.summary?.trim();
if (summary) {
const dissentMatch = DISSENT_PATTERNS.find((pattern) => pattern.test(summary));
if (dissentMatch) {
const matchText = summary.match(dissentMatch)?.[0] ?? dissentMatch.source;
const reason = `summary indicates incomplete work (${JSON.stringify(matchText)})`;
return {
ok: false,
refusalClass: "summary-claims-incomplete",
reason,
message: formatTaskDoneRefusal("summary-claims-incomplete", reason),
};
}
const scopedPattern = /\b(incomplete|not implemented|not done|not finished)\b/i;
const scopedMatch = scopedPattern.exec(summary);
if (scopedMatch) {
const start = Math.max(0, scopedMatch.index - 40);
const end = Math.min(summary.length, scopedMatch.index + scopedMatch[0].length + 40);
const scopedWindow = summary.slice(start, end);
const hasFirstPersonContext = /\b(i|i['’]?m|i['’]?ve|my|we)\b/i.test(scopedWindow)
|| /\b(the task|this task)\b/i.test(scopedWindow);
if (hasFirstPersonContext) {
const reason = `summary indicates incomplete work (${JSON.stringify(scopedMatch[0])})`;
return {
ok: false,
refusalClass: "summary-claims-incomplete",
reason,
message: formatTaskDoneRefusal("summary-claims-incomplete", reason),
};
}
}
}
if (pendingSteps.length >= 2) {
const allPendingApproved = pendingSteps.every((stepIndex) => codeReviewVerdicts.get(stepIndex) === "APPROVE");
if (!allPendingApproved) {
const reason = `attempted to auto-complete ${pendingSteps.length} pending steps without APPROVE verdicts on all of them`;
return {
ok: false,
refusalClass: "bulk-step-completion-without-review",
reason,
message: formatTaskDoneRefusal("bulk-step-completion-without-review", reason),
};
}
}
return { ok: true };
}
/**
* Determines the step index from which revision should restart given a set of
* completed steps and user feedback. Exported for unit tests; no longer called
@@ -3424,7 +3526,7 @@ export class TaskExecutor {
this.createTaskLogTool(task.id),
this.createTaskCreateTool(),
this.createTaskAddDepTool(task.id),
this.createTaskDoneTool(task.id, worktreePath, detail.prompt ?? "", () => { taskDone = true; }),
this.createTaskDoneTool(task.id, worktreePath, detail.prompt ?? "", codeReviewVerdicts, () => { taskDone = true; }),
createRunVerificationTool({
worktreePath,
rootDir: this.rootDir,
@@ -5256,7 +5358,13 @@ export class TaskExecutor {
return { blocked: false };
}
private createTaskDoneTool(taskId: string, worktreePath: string, promptContent: string, onDone: () => void): ToolDefinition {
private createTaskDoneTool(
taskId: string,
worktreePath: string,
promptContent: string,
codeReviewVerdicts: Map<number, ReviewVerdict>,
onDone: () => void,
): ToolDefinition {
const store = this.store;
return {
name: "fn_task_done",
@@ -5335,6 +5443,58 @@ export class TaskExecutor {
};
}
const taskDoneRefusal = evaluateTaskDoneRefusal(task, params, codeReviewVerdicts);
if (!taskDoneRefusal.ok) {
const refusalMessage = taskDoneRefusal.message;
await store.logEntry(taskId, refusalMessage, undefined, this.currentRunContext);
executorLog.error(`${taskId}: fn_task_done refused (${taskDoneRefusal.refusalClass}) — ${taskDoneRefusal.reason}`);
const priorRequeues = task.taskDoneRetryCount ?? 0;
const nextRequeueCount = priorRequeues + 1;
if (priorRequeues < MAX_TASK_DONE_REQUEUE_RETRIES) {
await store.updateTask(taskId, {
status: "failed",
error: refusalMessage,
taskDoneRetryCount: nextRequeueCount,
paused: false,
pausedByAgentId: null,
worktree: null,
branch: null,
sessionFile: null,
});
await store.logEntry(
taskId,
`${refusalMessage} — requeued to todo immediately (${nextRequeueCount}/${MAX_TASK_DONE_REQUEUE_RETRIES})`,
undefined,
this.currentRunContext,
);
await store.moveTask(taskId, "todo", { preserveProgress: true });
executorLog.log(`✗ ${taskId} fn_task_done refusal (${taskDoneRefusal.refusalClass}) — requeued to todo (${nextRequeueCount}/${MAX_TASK_DONE_REQUEUE_RETRIES})`);
} else {
await store.updateTask(taskId, {
status: "failed",
error: refusalMessage,
paused: false,
pausedByAgentId: null,
worktree: null,
branch: null,
sessionFile: null,
});
await store.logEntry(taskId, `${refusalMessage} — moved to in-review for inspection`, undefined, this.currentRunContext);
await this.persistTokenUsage(taskId);
await store.moveTask(taskId, "in-review");
executorLog.log(`✗ ${taskId} fn_task_done refusal (${taskDoneRefusal.refusalClass}) — moved to in-review for inspection`);
}
return {
content: [{ type: "text" as const, text: refusalMessage }],
details: {
error: refusalMessage,
refusalClass: taskDoneRefusal.refusalClass,
},
};
}
const settings = await store.getSettings();
const scopeLeakCheck = await this.evaluateTaskDoneScopeLeak(task, worktreePath, promptContent, settings)
.catch((error: unknown) => {