fix(FN-4482): harden scope guard typing and prompt source

Fusion-Task-Id: FN-4482
Fusion-Task-Lineage: 5850cda2-ded5-42ac-a206-396839580748
This commit is contained in:
Fusion
2026-05-14 14:21:16 -07:00
committed by gsxdsm
parent ffd274d646
commit 61cfb115c0
2 changed files with 8 additions and 7 deletions

View File

@@ -91,7 +91,7 @@ describe("FN-4482 plan-only scope leak guard", () => {
const { store, tool } = await setup({ unstaged: ["docs/foo.md"] });
const result = await tool.execute("id", {});
expect(result.content[0].text).toContain("Task marked complete");
expect(store.logEntry.mock.calls.some(([_, message]) => String(message).includes("[scope-leak] reviewLevel="))).toBe(false);
expect(store.logEntry.mock.calls.some((call: unknown[]) => String(call[1]).includes("[scope-leak] reviewLevel="))).toBe(false);
});
it("warns but allows plan-only off-scope edits in default warn mode", async () => {
@@ -131,7 +131,7 @@ describe("FN-4482 plan-only scope leak guard", () => {
const { store, tool } = await setup({ enforcement: "off", unstaged: ["packages/core/src/db.ts"] });
const result = await tool.execute("id", {});
expect(result.content[0].text).toContain("Task marked complete");
expect(store.logEntry.mock.calls.some(([_, message]) => String(message).includes("[scope-leak] reviewLevel="))).toBe(false);
expect(store.logEntry.mock.calls.some((call: unknown[]) => String(call[1]).includes("[scope-leak] reviewLevel="))).toBe(false);
});
it.each([0, 2])("uses warn-only behavior for non-plan-only review level %s", async (reviewLevel) => {
@@ -151,6 +151,6 @@ describe("FN-4482 plan-only scope leak guard", () => {
const result = await tool.execute("id", {});
expect(result.content[0].text).toContain("Task marked complete");
expect((executorLog.warn as any).mock.calls.some(([message]: [string]) => message.includes("Failed to capture uncommitted modified files"))).toBe(true);
expect(store.logEntry.mock.calls.some(([_, message]) => String(message).includes("[scope-leak] reviewLevel="))).toBe(false);
expect(store.logEntry.mock.calls.some((call: unknown[]) => String(call[1]).includes("[scope-leak] reviewLevel="))).toBe(false);
});
});

View File

@@ -3266,7 +3266,7 @@ export class TaskExecutor {
this.createTaskLogTool(task.id),
this.createTaskCreateTool(),
this.createTaskAddDepTool(task.id),
this.createTaskDoneTool(task.id, worktreePath, () => { taskDone = true; }),
this.createTaskDoneTool(task.id, worktreePath, detail.prompt ?? "", () => { taskDone = true; }),
createRunVerificationTool({
worktreePath,
rootDir: this.rootDir,
@@ -4875,6 +4875,7 @@ export class TaskExecutor {
private async evaluateTaskDoneScopeLeak(
task: Task,
worktreePath: string,
promptContent: string,
settings: Settings,
): Promise<{ blocked: false } | { blocked: true; message: string }> {
if (task.scopeOverride === true) {
@@ -4888,7 +4889,7 @@ export class TaskExecutor {
return { blocked: false };
}
const reviewLevel = parseReviewLevelFromPrompt(task.prompt ?? "");
const reviewLevel = parseReviewLevelFromPrompt(promptContent);
const configuredMode = settings.planOnlyScopeLeakEnforcement ?? "warn";
const enforcementMode: "off" | "warn" | "block" = reviewLevel === 1
? configuredMode
@@ -4928,7 +4929,7 @@ export class TaskExecutor {
return { blocked: false };
}
private createTaskDoneTool(taskId: string, worktreePath: string, onDone: () => void): ToolDefinition {
private createTaskDoneTool(taskId: string, worktreePath: string, promptContent: string, onDone: () => void): ToolDefinition {
const store = this.store;
return {
name: "fn_task_done",
@@ -5008,7 +5009,7 @@ export class TaskExecutor {
}
const settings = await store.getSettings();
const scopeLeakCheck = await this.evaluateTaskDoneScopeLeak(task, worktreePath, settings)
const scopeLeakCheck = await this.evaluateTaskDoneScopeLeak(task, worktreePath, promptContent, settings)
.catch((error: unknown) => {
const errorMessage = error instanceof Error ? error.message : String(error);
executorLog.warn(`${taskId}: scope-leak guard failed open: ${errorMessage}`);