From e2702bab6b8ce4fcfcb58fd0390a08e76906e10a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 30 Jun 2026 18:37:10 -0700 Subject: [PATCH] feat(workflows): add reviewer inline fixes --- .changeset/reviewer-inline-fixes.md | 7 ++ .../builtin-workflow-settings-triage.test.ts | 9 ++ .../core/src/builtin-workflow-settings.ts | 12 +++ .../executor-browser-verification.test.ts | 76 ++++++++++++++++- .../engine/src/__tests__/reviewer.test.ts | 45 +++++++++- packages/engine/src/agent-tools.ts | 38 ++++++++- packages/engine/src/executor.ts | 85 +++++++++++++++++-- packages/engine/src/reviewer.ts | 52 ++++++++++-- packages/engine/src/triage.ts | 1 + 9 files changed, 311 insertions(+), 14 deletions(-) create mode 100644 .changeset/reviewer-inline-fixes.md diff --git a/.changeset/reviewer-inline-fixes.md b/.changeset/reviewer-inline-fixes.md new file mode 100644 index 0000000000..8fb6b802b9 --- /dev/null +++ b/.changeset/reviewer-inline-fixes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Let workflow review nodes fix issues in the same reviewer session by default. +category: feature +dev: Adds reviewerInlineFixes workflow setting; off restores REVISE-to-remediation behavior. diff --git a/packages/core/src/__tests__/builtin-workflow-settings-triage.test.ts b/packages/core/src/__tests__/builtin-workflow-settings-triage.test.ts index 351ea60c71..cb302decdd 100644 --- a/packages/core/src/__tests__/builtin-workflow-settings-triage.test.ts +++ b/packages/core/src/__tests__/builtin-workflow-settings-triage.test.ts @@ -54,9 +54,18 @@ describe("workflow-native built-in workflow settings", () => { const movedKeyIds = new Set(MOVED_SETTINGS_KEYS); expect(BUILTIN_REVIEW_REVISION_SETTINGS.map((setting) => setting.id)).toEqual([ + "reviewerInlineFixes", "planReviewMaxRevisions", "codeReviewMaxRevisions", ]); + const inlineFixes = revisionById.get("reviewerInlineFixes"); + expect(inlineFixes).toMatchObject({ + type: "boolean", + default: true, + }); + expect(fullIds.has("reviewerInlineFixes")).toBe(true); + expect(movedIds.has("reviewerInlineFixes")).toBe(false); + expect(movedKeyIds.has("reviewerInlineFixes")).toBe(false); for (const id of ["planReviewMaxRevisions", "codeReviewMaxRevisions"]) { const setting = revisionById.get(id); expect(setting, `${id} should be declared`).toBeDefined(); diff --git a/packages/core/src/builtin-workflow-settings.ts b/packages/core/src/builtin-workflow-settings.ts index 397e143386..24979deef0 100644 --- a/packages/core/src/builtin-workflow-settings.ts +++ b/packages/core/src/builtin-workflow-settings.ts @@ -376,6 +376,18 @@ export const BUILTIN_TRIAGE_POLICY_SETTINGS: WorkflowSettingDefinition[] = [ ]; export const BUILTIN_REVIEW_REVISION_SETTINGS: WorkflowSettingDefinition[] = [ + { + id: "reviewerInlineFixes", + name: "Reviewer inline fixes", + type: "boolean", + default: true, + /* + * FNXC:WorkflowReviewers 2026-07-01-12:33: + * Default Coding reviewers should fix issues in the same review session when possible instead of always returning REVISE and bouncing the task back through executor remediation. Operators can turn this off per workflow to restore the old review-only behavior. + */ + description: + "Allow review-type workflow nodes to fix issues in their own reviewer session before returning a final verdict. Turn off to route findings back to executor remediation.", + }, { id: "planReviewMaxRevisions", name: "Plan Review revision cap", diff --git a/packages/engine/src/__tests__/executor-browser-verification.test.ts b/packages/engine/src/__tests__/executor-browser-verification.test.ts index a0170a0705..4470312dc7 100644 --- a/packages/engine/src/__tests__/executor-browser-verification.test.ts +++ b/packages/engine/src/__tests__/executor-browser-verification.test.ts @@ -18,12 +18,15 @@ import { type CapturedSession = { skillSelection?: { requestedSkillNames?: string[]; projectRootDir?: string; sessionPurpose?: string }; + tools?: "coding" | "readonly"; + systemPrompt?: string; + customTools?: Array<{ name?: string }>; }; function captureSession(output = '{"verdict":"APPROVE","notes":""}') { const holder: { last?: CapturedSession } = {}; mockedCreateFnAgent.mockImplementation(async (opts: any) => { - holder.last = { skillSelection: opts.skillSelection }; + holder.last = { skillSelection: opts.skillSelection, tools: opts.tools, systemPrompt: opts.systemPrompt, customTools: opts.customTools }; const listeners: Array<(event: any) => void> = []; return { session: { @@ -112,6 +115,24 @@ function planReviewStep(overrides: Record = {}) { }; } +function codeReviewStep(overrides: Record = {}) { + return { + id: "graph:code-review-step", + name: "Code Review", + description: "", + mode: "prompt", + phase: "pre-merge", + gateMode: "gate", + prompt: "Review the code.", + toolMode: "readonly", + enabled: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + optionalGroupId: "code-review", + ...overrides, + }; +} + describe("browser-verification workflow-step browser capability", () => { beforeEach(() => { resetExecutorMocks(); @@ -311,4 +332,57 @@ describe("browser-verification workflow-step browser capability", () => { expect.stringContaining("Plan Review deterministic external-integration evidence check requested revision"), ); }); + + it("lets review-type workflow steps fix inline by default and respects the off switch", async () => { + const store = createMockStore(); + const executor = makeExecutor(store); + const cap = captureSession(); + + const enabledResult = await (executor as any).executeWorkflowStep( + baseTask(), + codeReviewStep(), + "/tmp/wt", + { reviewerInlineFixes: true }, + undefined, + undefined, + ); + + expect(enabledResult.success).toBe(true); + expect(cap.last?.tools).toBe("coding"); + expect(cap.last?.systemPrompt).toContain("Same-Session Fix Policy"); + + const offCap = captureSession(); + const disabledResult = await (executor as any).executeWorkflowStep( + baseTask(), + codeReviewStep(), + "/tmp/wt", + { reviewerInlineFixes: false }, + undefined, + undefined, + ); + + expect(disabledResult.success).toBe(true); + expect(offCap.last?.tools).toBe("readonly"); + expect(offCap.last?.systemPrompt).not.toContain("Same-Session Fix Policy"); + }); + + it("keeps Plan Review readonly while allowing PROMPT.md inline repair", async () => { + const store = createMockStore(); + const executor = makeExecutor(store); + const cap = captureSession(); + + const result = await (executor as any).executeWorkflowStep( + baseTask(), + planReviewStep(), + "/tmp/wt", + { reviewerInlineFixes: true }, + undefined, + undefined, + ); + + expect(result.success).toBe(true); + expect(cap.last?.tools).toBe("readonly"); + expect(cap.last?.customTools?.map((tool) => tool.name)).toContain("fn_task_prompt_write"); + expect(cap.last?.systemPrompt).toContain("fn_task_prompt_write"); + }); }); diff --git a/packages/engine/src/__tests__/reviewer.test.ts b/packages/engine/src/__tests__/reviewer.test.ts index bfb9408ae4..5b42e943c8 100644 --- a/packages/engine/src/__tests__/reviewer.test.ts +++ b/packages/engine/src/__tests__/reviewer.test.ts @@ -276,6 +276,47 @@ describe("reviewStep — spec review type", () => { expect(opts.systemPrompt).toContain("Mission clarity"); }); + it("allows same-session reviewer fixes when requested", async () => { + mockedCreateFnAgent.mockResolvedValue( + createMockSession("### Verdict: APPROVE\n### Summary\nFixed the plan."), + ); + const store = { + updateTask: vi.fn().mockResolvedValue(undefined), + logEntry: vi.fn().mockResolvedValue(undefined), + appendAgentLog: vi.fn().mockResolvedValue(undefined), + }; + + await reviewStep( + "/tmp/worktree", "FN-050", 0, "Plan Review", "plan", "# Task: KB-050", + undefined, + { allowInlineFixes: true, store: store as any, taskId: "FN-050" }, + ); + + expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1); + const opts = mockedCreateFnAgent.mock.calls[0][0]; + expect(opts.tools).toBe("readonly"); + expect(opts.customTools?.map((tool: any) => tool.name)).toContain("fn_task_prompt_write"); + expect(mockedPromptWithFallback.mock.calls[0][1]).toContain("Same-Session Fix Policy"); + expect(mockedPromptWithFallback.mock.calls[0][1]).toContain("fn_task_prompt_write"); + }); + + it("uses coding tools for same-session code review fixes", async () => { + mockedCreateFnAgent.mockResolvedValue( + createMockSession("### Verdict: APPROVE\n### Summary\nFixed the code."), + ); + + await reviewStep( + "/tmp/worktree", "FN-051", 1, "Code Review", "code", "# Task: KB-051", + undefined, + { allowInlineFixes: true }, + ); + + expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1); + const opts = mockedCreateFnAgent.mock.calls[0][0]; + expect(opts.tools).toBe("coding"); + expect(opts.customTools?.map((tool: any) => tool.name)).not.toContain("fn_task_prompt_write"); + }); + it("appends reviewer plugin prompt contributions when provided", async () => { mockedCreateFnAgent.mockResolvedValue( createMockSession("### Verdict: APPROVE\n### Summary\nGood spec."), @@ -540,7 +581,7 @@ describe("reviewStep — context-limit retry", () => { "code", promptContent, "abc123", - { store: store as any, taskId: "FN-4082", userComments }, + { store: store as any, taskId: "FN-4082", userComments, allowInlineFixes: true }, ); expect(result.verdict).toBe("APPROVE"); @@ -554,6 +595,8 @@ describe("reviewStep — context-limit retry", () => { expect(secondRequest).toContain("### Step 1: Compact prompt"); expect(secondRequest).toContain("## User Comments"); expect(secondRequest).toContain("User says compact retry must keep this requirement."); + expect(firstRequest).toContain("Same-Session Fix Policy"); + expect(secondRequest).toContain("Same-Session Fix Policy"); expect(secondRequest.match(/## User Comments/g)).toHaveLength(1); expect(store.logEntry).toHaveBeenCalledWith( "FN-4082", diff --git a/packages/engine/src/agent-tools.ts b/packages/engine/src/agent-tools.ts index 532722b29f..6253a16cdf 100644 --- a/packages/engine/src/agent-tools.ts +++ b/packages/engine/src/agent-tools.ts @@ -95,6 +95,10 @@ export const taskDocumentReadParams = Type.Object({ ), }); +export const taskPromptWriteParams = Type.Object({ + content: Type.String({ description: "Complete replacement content for this task's PROMPT.md." }), +}); + export const chatTaskDocumentWriteParams = Type.Object({ task_id: Type.String({ description: "Task ID to write the document to (e.g. 'FN-001')." }), key: Type.String({ @@ -1289,6 +1293,39 @@ export function createTaskDocumentReadTool(store: TaskStore, taskId: string): To }; } +/** + * FNXC:WorkflowReviewers 2026-07-01-13:22: + * Plan Review inline fixes must be able to rewrite the task's authoritative PROMPT.md, but that pre-execution reviewer should not need general source-file write tools. Route the write through TaskStore so existing PROMPT.md validation, task directory placement, and task.json sync remain the single persistence path. + */ +export function createTaskPromptWriteTool(store: TaskStore, taskId: string, runContext?: RunMutationContext): ToolDefinition { + return { + name: "fn_task_prompt_write", + label: "Write PROMPT.md", + description: + "Replace this task's PROMPT.md with revised plan/spec content. " + + "Use only during Plan Review/spec repair; provide the complete final PROMPT.md content.", + parameters: taskPromptWriteParams, + execute: async (_id: string, params: Static) => { + try { + await store.updateTask(taskId, { prompt: params.content }, runContext); + return { + content: [{ type: "text" as const, text: `Updated PROMPT.md for ${taskId}.` }], + details: {}, + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + return { + content: [{ + type: "text" as const, + text: `ERROR: Failed to update PROMPT.md for ${taskId}: ${err.message}`, + }], + details: {}, + }; + } + }, + }; +} + /** * FNXC:ChatAgentTools 2026-06-18-06:51: * Chat sessions do not have an ambient task, but users expect the same `fn_task_document_write` and `fn_task_document_read` names that task-bound lanes expose. @@ -4229,4 +4266,3 @@ export function createAcquireRepoWorktreeTool(opts: { }, }; } - diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 9b023449be..85e4181a2c 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -203,6 +203,7 @@ import { createTaskCreateTool as sharedCreateTaskCreateTool, createTaskDocumentReadTool as sharedCreateTaskDocumentReadTool, createTaskDocumentWriteTool as sharedCreateTaskDocumentWriteTool, + createTaskPromptWriteTool as sharedCreateTaskPromptWriteTool, createTaskLogTool as sharedCreateTaskLogTool, createWorkflowListTool as sharedCreateWorkflowListTool, createWorkflowGetTool as sharedCreateWorkflowGetTool, @@ -7072,13 +7073,34 @@ export class TaskExecutor { const rawCliCommand = executorKind === "cli" && typeof cfg.cliCommand === "string" && cfg.cliCommand.trim() ? cfg.cliCommand.trim() : undefined; + const nodeNameForReviewDetection = typeof cfg.name === "string" && cfg.name.trim() ? cfg.name.trim() : node.id; + const isPlanReviewNode = + node.id === "plan-review-step" + || nodeNameForReviewDetection === "Plan Review" + || optionalGroupId === "plan-review"; + const inlineFixesEnabledForNode = (settings as Settings & { reviewerInlineFixes?: boolean }).reviewerInlineFixes !== false; + const reviewTypeNode = + isPlanReviewNode + || cfg.reviewCanFixInline === true + || /(?:^|\b)(?:review|verification)(?:\b|$)/i.test(nodeNameForReviewDetection) + || optionalGroupId === "code-review" + || optionalGroupId === "browser-verification"; + const inlineFixesMakeNodeWriteCapable = + inlineFixesEnabledForNode + && executorKind !== "cli" + && reviewTypeNode + && !isPlanReviewNode; // Isolation guard: write-capable nodes must run inside a task worktree, not // the shared repo root. Before the execute seam runs, live.worktree is unset // — a coding/script/CLI node falling back to this.rootDir would mutate the // main checkout and cross-contaminate other tasks. Reject such nodes until a // worktree exists. Read-only nodes (default toolMode) are safe against root. - const writeCapable = cfg.toolMode === "coding" || node.kind === "script" || Boolean(scriptName) || Boolean(rawCliCommand); + /* + FNXC:WorkflowReviewers 2026-07-01-13:28: + Inline-fix Code Review, Browser Verification, and custom review nodes become write-capable even when the workflow definition says `toolMode: readonly`, so the isolation guard must see that before selecting a worktree. Plan Review is excluded because it uses the narrow PROMPT.md writer instead of source-file write tools. + */ + const writeCapable = cfg.toolMode === "coding" || inlineFixesMakeNodeWriteCapable || node.kind === "script" || Boolean(scriptName) || Boolean(rawCliCommand); const executionTarget = writeCapable ? await this.store.getTask(live.id) : live; if (writeCapable && !executionTarget.worktree && !this.workspaceConfig) { return { outcome: "failure", value: "no-worktree-for-write-node" }; @@ -7251,6 +7273,12 @@ export class TaskExecutor { if (cfg.requireExternalIntegrationEvidence === true) { (step as WorkflowStep & { requireExternalIntegrationEvidence?: boolean }).requireExternalIntegrationEvidence = true; } + if (optionalGroupId) { + (step as WorkflowStep & { optionalGroupId?: string }).optionalGroupId = optionalGroupId; + } + if (cfg.reviewCanFixInline === true) { + (step as WorkflowStep & { reviewCanFixInline?: boolean }).reviewCanFixInline = true; + } // (U8a) Thread the plugin-injected runtime env (FUSION_CE_SKILLS_DIR / // FUSION_CE_AGENTS_DIR + PATH contribution) into prompt-mode skill/model @@ -11765,6 +11793,10 @@ export class TaskExecutor { return sharedCreateTaskDocumentReadTool(this.store, taskId); } + private createTaskPromptWriteTool(taskId: string): ToolDefinition { + return sharedCreateTaskPromptWriteTool(this.store, taskId, this.getRunContextFor(taskId)); + } + private createArtifactRegisterTool(authorId: string): ToolDefinition { return sharedCreateArtifactRegisterTool(this.store, authorId, this.options.messageStore); } @@ -14023,14 +14055,37 @@ ${scopeGuard} taskEnv?: NodeJS.ProcessEnv, stepOptions?: { unattended?: boolean }, ): Promise { - const toolMode: "coding" | "readonly" = workflowStep.toolMode || "readonly"; + let toolMode: "coding" | "readonly" = workflowStep.toolMode || "readonly"; // (U3) Genuinely-unattended run — set FUSION_HEADLESS=1 below so skills record // assumptions and proceed instead of parking on a question. Explicit opt-in // only (default false = board run); see runGraphCustomNode / KTD-3. const unattended = stepOptions?.unattended === true; const isPlanReviewStep = workflowStep.id === "graph:plan-review-step" || workflowStep.name === "Plan Review"; + const workflowStepMetadata = workflowStep as WorkflowStep & { + optionalGroupId?: string; + reviewCanFixInline?: boolean; + requireExternalIntegrationEvidence?: boolean; + }; + const optionalGroupId = workflowStepMetadata.optionalGroupId; + const isReviewTypeWorkflowStep = + isPlanReviewStep + || workflowStepMetadata.reviewCanFixInline === true + || /(?:^|\b)(?:review|verification)(?:\b|$)/i.test(workflowStep.name) + || optionalGroupId === "plan-review" + || optionalGroupId === "code-review" + || optionalGroupId === "browser-verification"; + const reviewerInlineFixesEnabled = (settings as Settings & { reviewerInlineFixes?: boolean }).reviewerInlineFixes !== false; + const allowReviewerInlineFixes = reviewerInlineFixesEnabled && isReviewTypeWorkflowStep && workflowStep.mode === "prompt"; + const allowPlanReviewPromptWrite = allowReviewerInlineFixes && isPlanReviewStep; + if (allowReviewerInlineFixes && !isPlanReviewStep) { + /* + * FNXC:WorkflowReviewers 2026-07-01-12:36: + * Review-type workflow nodes can now repair their own findings when the workflow setting `reviewerInlineFixes` is on. Use coding tools for implementation review sessions so Code Review, Browser Verification, and custom review/verification gates do not have to bounce through executor remediation for issues they can safely fix inline. Plan Review stays on a narrow PROMPT.md writer because it runs before implementation. + */ + toolMode = "coding"; + } const requireExternalIntegrationEvidence = - (workflowStep as WorkflowStep & { requireExternalIntegrationEvidence?: boolean }).requireExternalIntegrationEvidence === true; + workflowStepMetadata.requireExternalIntegrationEvidence === true; if (isPlanReviewStep && requireExternalIntegrationEvidence) { /* @@ -14151,6 +14206,18 @@ verdict JSON object — this step does not gate merge. If you need to ask the us a question, emit a single ===FUSION_AWAIT_INPUT=== block and stop (see the workflow-step conventions in your instructions).`; + const inlineFixBlock = allowReviewerInlineFixes + ? ` + +## Same-Session Fix Policy + +This review-type node may fix issues it finds before returning a final verdict. +- If you find an in-scope issue you can fix safely, edit the relevant files in this same session, run the smallest relevant verification, and then return APPROVE or APPROVE_WITH_NOTES. +- Return REVISE only when the issue is still present, cannot be safely fixed in this reviewer session, needs broader executor remediation, or needs user input. +- Plan Review may use fn_task_prompt_write to replace the task's PROMPT.md with the complete revised plan. Do not implement product code from Plan Review. +- Code Review and Browser Verification may fix implementation issues inside the assigned task worktree and should mention the fix in notes.` + : ""; + const systemPrompt = `You are a workflow step agent executing: ${workflowStep.name} Task Context: @@ -14168,7 +14235,7 @@ Your role: Your Instructions: ${workflowStep.prompt} -You have access to the file system to review changes.${verdictBlock}`; +You have access to the file system to review changes.${inlineFixBlock}${verdictBlock}`; const agentLogger = new AgentLogger({ store: this.store, @@ -14331,12 +14398,18 @@ You have access to the file system to review changes.${verdictBlock}`; // (a dedicated readonly-plus-spawn tool mode) is deferred; this is a // knowingly-accepted gap, not a closed one — re-evaluate before enabling the // CE workflow for genuinely-unattended (FUSION_HEADLESS) LFG/pipeline runs. + const planReviewPromptTools: ToolDefinition[] = allowPlanReviewPromptWrite + ? [this.createTaskPromptWriteTool(task.id)] + : []; const codingCustomTools: ToolDefinition[] = toolMode === "coding" ? [this.createSpawnAgentTool(task.id, worktreePath, settings, stepEnv)] : []; + const workflowCustomTools = [...planReviewPromptTools, ...codingCustomTools]; const readonlyCustomTools = toolMode === "readonly" - ? filterCustomToolsForReadonly(codingCustomTools) - : { allowed: codingCustomTools, denied: [] as string[] }; + ? filterCustomToolsForReadonly(workflowCustomTools, { + allowTool: (tool) => allowPlanReviewPromptWrite && tool.name === "fn_task_prompt_write", + }) + : { allowed: workflowCustomTools, denied: [] as string[] }; if (toolMode === "readonly" && readonlyCustomTools.denied.length > 0) { await this.store.logEntry( task.id, diff --git a/packages/engine/src/reviewer.ts b/packages/engine/src/reviewer.ts index 19b6a00ffc..a31226fbba 100644 --- a/packages/engine/src/reviewer.ts +++ b/packages/engine/src/reviewer.ts @@ -33,7 +33,7 @@ import { import { buildPromptLayers, collapsePromptLayers } from "./prompt-layers.js"; import { createFallbackModelObserver } from "./fallback-model-observer.js"; import { createRunAuditor, generateSyntheticRunId } from "./run-audit.js"; -import { createMemoryGetTool, createMemorySearchTool, createWebFetchTool } from "./agent-tools.js"; +import { createMemoryGetTool, createMemorySearchTool, createTaskPromptWriteTool, createWebFetchTool } from "./agent-tools.js"; import { buildUserCommentsPromptSection } from "./agent-user-comments.js"; import { resolveMcpServersForStore } from "./mcp-resolution.js"; @@ -100,6 +100,8 @@ export interface ReviewOptions { settings?: Settings; /** Plugin runner for runtime selection. When provided, enables plugin runtime lookup. */ pluginRunner?: import("./plugin-runner.js").PluginRunner; + /** Allow this reviewer to fix in-scope findings in the same session before returning its final verdict. */ + allowInlineFixes?: boolean; /** * Fired immediately after the reviewer's `AgentSession` is created. The * caller can register the session in a per-task subagent map so that the @@ -115,6 +117,25 @@ export interface ReviewOptions { onSessionEnded?: (session: import("@earendil-works/pi-coding-agent").AgentSession) => void; } +function buildSameSessionFixPolicy(reviewType: ReviewType, canWritePrompt: boolean): string { + const planSpecInstruction = canWritePrompt + ? "- For plan/spec review, use fn_task_prompt_write with the complete revised PROMPT.md when the plan artifact needs repair. Do not implement product code from plan/spec review." + : "- For plan/spec review, limit fixes to task-planning context available in this session. Do not implement product code from plan/spec review."; + const codeInstruction = "- For code review, fix implementation issues inside the assigned task worktree and mention the fix in your review notes."; + return ` + +## Same-Session Fix Policy + +This review may fix issues it finds before returning a final verdict. +- If you find an in-scope issue you can fix safely, edit the relevant file(s) in this same reviewer session, run the smallest relevant verification, and then return APPROVE or APPROVE_WITH_NOTES. +- Return REVISE only when the issue is still present, cannot be safely fixed in this reviewer session, needs broader executor remediation, or needs user input. +${reviewType === "code" ? codeInstruction : planSpecInstruction}`; +} + +function appendSameSessionFixPolicy(request: string, reviewType: ReviewType, canWritePrompt: boolean): string { + return `${request}${buildSameSessionFixPolicy(reviewType, canWritePrompt)}`; +} + /** * Spawn a reviewer agent to evaluate a worker's plan or code for a step. * @@ -164,9 +185,21 @@ export async function reviewStep( }; } - const request = buildReviewRequest( + const canWritePromptInline = + options.allowInlineFixes === true + && reviewType !== "code" + && Boolean(options.store && options.taskId); + + let request = buildReviewRequest( taskId, stepNumber, stepName, reviewType, promptContent, cwd, baseline, options.userComments, ); + if (options.allowInlineFixes === true) { + /* + * FNXC:WorkflowReviewers 2026-07-01-12:39: + * Triage Plan Review uses this reviewer path instead of graph `executeWorkflowStep`. When workflow setting `reviewerInlineFixes` is enabled, the reviewer must be allowed to repair PROMPT.md/spec findings in this same session and return the final verdict after the fix. + */ + request = appendSameSessionFixPolicy(request, reviewType, canWritePromptInline); + } const effectiveSettings = liveSettings ?? options.settings; const agentLogger = options.store && options.taskId @@ -359,6 +392,12 @@ export async function reviewStep( source: "reviewer", }) : undefined; + const reviewCustomTools = [ + createWebFetchTool(), + ...(canWritePromptInline && options.store && options.taskId ? [createTaskPromptWriteTool(options.store, options.taskId)] : []), + ...(memoryTools ?? []), + ]; + const { session } = await createResolvedAgentSession({ sessionPurpose: "reviewer", runtimeHint: extractRuntimeHint(memoryAgent?.runtimeConfig), @@ -366,8 +405,8 @@ export async function reviewStep( cwd, systemPrompt: reviewerSystemPromptFinal, systemPromptLayers: layers, - tools: "readonly", - customTools: [createWebFetchTool(), ...(memoryTools ?? [])], + tools: options.allowInlineFixes === true && reviewType === "code" ? "coding" : "readonly", + customTools: reviewCustomTools, onText: handleReviewerText, onThinking: agentLogger?.onThinking, onToolStart: agentLogger?.onToolStart, @@ -479,9 +518,12 @@ export async function reviewStep( } reviewText = ""; - const reducedRequest = buildReducedReviewRequest( + let reducedRequest = buildReducedReviewRequest( taskId, stepNumber, stepName, reviewType, promptContent, cwd, baseline, options.userComments, ); + if (options.allowInlineFixes === true) { + reducedRequest = appendSameSessionFixPolicy(reducedRequest, reviewType, canWritePromptInline); + } try { await runReviewPrompt(session, reducedRequest); diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index 0199bf277c..2f684901a0 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -1996,6 +1996,7 @@ export class TriageProcessor { rootDir: this.rootDir, agentStore: this.options.agentStore, pluginRunner: this.options.pluginRunner, + allowInlineFixes: (settings as Settings & { reviewerInlineFixes?: boolean }).reviewerInlineFixes !== false, onSessionCreated: (session) => this.registerSubagentSession(task.id, session), onSessionEnded: (session) => this.unregisterSubagentSession(task.id, session), },