diff --git a/.changeset/graph-review-gates-own-execution.md b/.changeset/graph-review-gates-own-execution.md new file mode 100644 index 0000000000..6a59d17a89 --- /dev/null +++ b/.changeset/graph-review-gates-own-execution.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Prevent workflow tasks from duplicating plan review during implementation. +category: fix +dev: Graph-owned execution sessions no longer receive or prompt for legacy per-step review tools. diff --git a/packages/engine/src/__tests__/executor-fast-mode-workflows.test.ts b/packages/engine/src/__tests__/executor-fast-mode-workflows.test.ts index a72d667ae3..6a00c72ddd 100644 --- a/packages/engine/src/__tests__/executor-fast-mode-workflows.test.ts +++ b/packages/engine/src/__tests__/executor-fast-mode-workflows.test.ts @@ -272,4 +272,31 @@ describe("fast mode workflow/runtime invariants", () => { const tools = mockedCreateFnAgent.mock.calls[0][0].customTools.map((tool: any) => tool.name); expect(tools).toContain("fn_review_step"); }); + + it("omits legacy fn_review_step in graph-owned standard execution sessions", async () => { + mockedCreateFnAgent.mockImplementation(async (opts: any) => ({ + session: { + prompt: vi.fn().mockResolvedValue(undefined), + dispose: vi.fn(), + sessionManager: { + getLeafId: vi.fn().mockReturnValue("leaf"), + branchWithSummary: vi.fn(), + navigateTree: vi.fn().mockResolvedValue({ cancelled: false }), + }, + navigateTree: vi.fn().mockResolvedValue({ cancelled: false }), + }, + capturedTools: opts.customTools, + })); + const store = createMockStore(); + const liveTask = task({ id: "FN-TOOLS", executionMode: "standard" }); + store.getTask.mockResolvedValue(liveTask); + const executor = new TaskExecutor(store, "/tmp/test") as any; + executor.graphCompletionInterceptors.set("FN-TOOLS", vi.fn()); + + await executor.execute(liveTask); + + const tools = mockedCreateFnAgent.mock.calls[0][0].customTools.map((tool: any) => tool.name); + expect(tools).toContain("fn_task_done"); + expect(tools).not.toContain("fn_review_step"); + }); }); diff --git a/packages/engine/src/__tests__/executor-prompt.test.ts b/packages/engine/src/__tests__/executor-prompt.test.ts index 9bcbdec0f2..0086c1a4c8 100644 --- a/packages/engine/src/__tests__/executor-prompt.test.ts +++ b/packages/engine/src/__tests__/executor-prompt.test.ts @@ -145,6 +145,38 @@ describe("buildExecutionPrompt", () => { expect(result).not.toContain("## Attachments"); }); + it("does not instruct graph-owned execution sessions to request per-step reviews", () => { + const task = createMockTaskDetail({ + prompt: [ + "# test", + "**Review Level:** 2", + "## Steps", + "### Step 0: Preflight", + "- [ ] check", + "### Step 1: Implement", + "- [ ] change code", + "### Step 2: Delivery", + "- [ ] summarize", + ].join("\n"), + }); + + const result = buildExecutionPrompt( + task, + "/home/user/project", + undefined, + undefined, + undefined, + undefined, + undefined, + { workflowReviewGatesOwnedByGraph: true }, + ); + + expect(result).toContain("Workflow review gates are handled by the workflow graph"); + expect(result).not.toContain("Before implementing each step"); + expect(result).not.toContain("After implementing + committing each step"); + expect(result).not.toContain("fn_review_step"); + }); + it("includes Custom fields section listing id/name/type, enum options, required, and current value", () => { const task = createMockTaskDetail({ customFields: { severity: "high" } }); const result = buildExecutionPrompt(task, "/home/user/project", undefined, undefined, undefined, [ diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index a28aaf8139..6b32d7bc9f 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -9560,8 +9560,11 @@ export class TaskExecutor { error: (s) => executorLog.warn(s), }, }), - // Skip fn_review_step tool in fast mode — fast mode bypasses automated review gates - ...(executionMode !== "fast" ? [ + /* + FNXC:WorkflowReviewGates 2026-06-29-20:41: + Workflow-graph execution owns plan/code/browser review gates as nodes. Do not expose legacy in-session `fn_review_step` during graph-owned execute seams; otherwise default coding can duplicate Plan Review inside implementation steps after the workflow-level Plan Review has already passed. + */ + ...(executionMode !== "fast" && !this.graphCompletionInterceptors.has(task.id) ? [ this.createReviewStepTool(task.id, worktreePath, detail.prompt, codeReviewVerdicts, sessionRef, stepCheckpoints, detail, stuckDetector), ] : []), this.createSpawnAgentTool(task.id, worktreePath, settings, taskEnv), @@ -9903,6 +9906,7 @@ export class TaskExecutor { this.options.pluginRunner, customFieldDefs, this.workspaceConfig, + { workflowReviewGatesOwnedByGraph: this.graphCompletionInterceptors.has(task.id) }, ); await promptWithFallback(session, agentPrompt); } @@ -10273,7 +10277,16 @@ export class TaskExecutor { "Do NOT ask for permission. Do NOT write a summary. Just call a tool and keep working.", "", "Original task:", - buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner, retryCustomFieldDefs, this.workspaceConfig), + buildExecutionPrompt( + detail, + this.rootDir, + settings, + worktreePath, + this.options.pluginRunner, + retryCustomFieldDefs, + this.workspaceConfig, + { workflowReviewGatesOwnedByGraph: this.graphCompletionInterceptors.has(task.id) }, + ), ].join("\n"); } else { retryPrompt = [ @@ -10283,7 +10296,16 @@ export class TaskExecutor { "2. If there is remaining work, finish it and then call fn_task_done.", "", "Original task:", - buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner, retryCustomFieldDefs, this.workspaceConfig), + buildExecutionPrompt( + detail, + this.rootDir, + settings, + worktreePath, + this.options.pluginRunner, + retryCustomFieldDefs, + this.workspaceConfig, + { workflowReviewGatesOwnedByGraph: this.graphCompletionInterceptors.has(task.id) }, + ), ].join("\n"); } @@ -16980,9 +17002,15 @@ export function buildExecutionPrompt( pluginRunner?: PluginRunner, customFieldDefs?: WorkflowFieldDefinition[], workspaceConfig?: WorkspaceConfig | null, + options?: { workflowReviewGatesOwnedByGraph?: boolean }, ): string { const prompt = scopePromptToWorktree(task.prompt, rootDir, worktreePath, workspaceConfig); const reviewLevel = parseReviewLevelFromPrompt(prompt); + /* + * FNXC:WorkflowReviewGates 2026-06-29-20:41: + * Default Coding and other workflow-graph tasks run review gates as graph nodes, so the executor prompt must not ask implementation agents to call legacy per-step review tools. This keeps Plan Review once-before-execution and Code Review once-before-merge unless a workflow explicitly adds a step-review node. + */ + const workflowReviewGatesOwnedByGraph = options?.workflowReviewGatesOwnedByGraph === true; // Build co-author trailer arg for git commits based on settings. The user's // configured git identity remains the primary author; Fusion is appended as @@ -17126,12 +17154,12 @@ ${prompt} ${attachmentsSection}${commandsSection}${memorySection}${progressSection}${steeringSection}${customFieldsSection} ## Review level: ${reviewLevel} -${reviewLevel === 0 ? "No reviews required. Implement directly." : ""} +${workflowReviewGatesOwnedByGraph ? `Workflow review gates are handled by the workflow graph outside this implementation session. Do not request per-step plan review or per-step code review from inside execution; complete the implementation steps and let the graph run enabled Plan Review, Browser Verification, and Code Review nodes at their configured positions.` : `${reviewLevel === 0 ? "No reviews required. Implement directly." : ""} ${reviewLevel >= 1 ? `Before implementing each step (except Step 0 and the final step), call: \`fn_review_step(step=N, type="plan", step_name="...")\`` : ""} ${reviewLevel >= 2 ? `After implementing + committing each step, call: \`fn_review_step(step=N, type="code", step_name="...", baseline="")\`` : ""} -${reviewLevel >= 3 ? `After tests, also call fn_review_step with type="code" for test review.` : ""} +${reviewLevel >= 3 ? `After tests, also call fn_review_step with type="code" for test review.` : ""}`} ${pluginTaskContributions ? ` ${pluginTaskContributions}