diff --git a/packages/engine/src/__tests__/executor-task-done-invariant.test.ts b/packages/engine/src/__tests__/executor-task-done-invariant.test.ts index 8cfa431ace..53043d66e5 100644 --- a/packages/engine/src/__tests__/executor-task-done-invariant.test.ts +++ b/packages/engine/src/__tests__/executor-task-done-invariant.test.ts @@ -93,7 +93,7 @@ async function setup(overrides: Record = {}) { }); const executor = new TaskExecutor(store as any, "/repo"); - await executor.execute(baseTask() as any); + await executor.execute(task as any); return { store, tool, setTask: (next: any) => (task = { ...task, ...next }) }; } @@ -217,7 +217,7 @@ describe("FN-4114 fn_task_done invariants", () => { evidence: expect.objectContaining({ reason: "invariant-check-failed" }), })); expect(store.logEntry).toHaveBeenCalledWith( - "FN-4114", + "FN-416", expect.stringContaining("prompt/source metadata derived operational no-commit contract"), undefined, undefined, @@ -226,6 +226,45 @@ describe("FN-4114 fn_task_done invariants", () => { expect(revListCalled).toBe(false); }); + + it("FN-416 refuses plan-only operational no-source completion when File Scope is missing", async () => { + const promptWithoutFileScope = `# Task: FN-417 - Assign ready implementation task to active owner + +## Review Level: 1 (Plan Only) + +**Assessment:** This is an operational routing task with no expected product-source changes. + +## Mission +Assign or route exactly one ready implementation task to an eligible active owner, or record an intentional no-route state. No source files expected. + +## Steps + +### Step 1: Route exactly one existing ready task or record no-route +- [x] Record evidence in task documents/logs +`; + const { store, tool } = await setup({ + id: "FN-417", + title: "Assign ready implementation task to active owner", + description: "Operational routing task with no expected product-source changes; record routing evidence or no-route state.", + reviewLevel: 1, + prompt: promptWithoutFileScope, + sourceMetadata: {}, + log: [{ timestamp: new Date().toISOString(), action: "Routing evidence recorded", outcome: "No-route state documented in task docs" }], + steps: [{ name: "Route or record no-route", status: "done" as const }], + }); + 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-4114\n"); + if (cmd.includes("rev-list --count")) return Buffer.from("0\n"); + if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n"); + return Buffer.from(""); + }); + + const result = await tool.execute("id", {}); + expect(result.content[0].text).toContain("fn_task_done refused: no_commits"); + expect(store.moveTask).toHaveBeenCalledWith("FN-417", "todo", { preserveProgress: true }); + }); + it("FN-416 keeps the missing-commit guard for source-changing plan-only tasks without an explicit contract", async () => { const { store, tool } = await setup({ title: "Implement executor fix", diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 0a649c5c58..a9021d3d44 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -656,7 +656,9 @@ function evaluatePromptDerivedNoCommitEligibility(task: Task, promptContent: str ? task.sourceMetadata.fileScope.filter((entry): entry is string => typeof entry === "string") : []; const declaredScope = [...promptScopeEntries, ...metadataScope]; + if (declaredScope.length === 0) return { eligible: false }; if (declaredScope.some(hasSourceChangingScopeEntry)) return { eligible: false }; + if (!declaredScope.every(isNoSourceScopeEntry)) return { eligible: false }; const stepsComplete = Array.isArray(task.steps) && task.steps.length > 0 ? task.steps.every((step) => step.status === "done" || step.status === "skipped")