feat(FN-2247): skip review and workflow gates in fast mode

- Gate executor pre-merge workflow-step execution on executionMode and bypass it for fast runs
- Omit the review_step tool from agent tool injection when tasks run in fast mode
- Add explicit executor logs and task log entries when fast mode skips validation gates
- Extend executor messaging-tool tests to cover fast vs standard tool availability
This commit is contained in:
Fusion
2026-04-22 10:50:22 -07:00
committed by gsxdsm
parent c1e74d86f4
commit b38db611c3
2 changed files with 122 additions and 45 deletions

View File

@@ -11189,8 +11189,9 @@ describe("TaskExecutor messaging tools", () => {
messageStore?: unknown; messageStore?: unknown;
agentStore?: unknown; agentStore?: unknown;
assignedAgentId?: string; assignedAgentId?: string;
executionMode?: "standard" | "fast";
}): Promise<any[]> { }): Promise<any[]> {
const { messageStore, agentStore, assignedAgentId } = options || {}; const { messageStore, agentStore, assignedAgentId, executionMode } = options || {};
let captured: any[] = []; let captured: any[] = [];
mockedCreateFnAgent.mockImplementation(async (opts: any) => { mockedCreateFnAgent.mockImplementation(async (opts: any) => {
@@ -11210,7 +11211,7 @@ describe("TaskExecutor messaging tools", () => {
}); });
const store = createMockStore(); const store = createMockStore();
// Override getTask to return the correct assignedAgentId // Override getTask to return the correct assignedAgentId and executionMode
store.getTask.mockImplementation(async (id: string) => ({ store.getTask.mockImplementation(async (id: string) => ({
id, id,
title: "Test", title: "Test",
@@ -11221,6 +11222,7 @@ describe("TaskExecutor messaging tools", () => {
currentStep: 0, currentStep: 0,
log: [], log: [],
assignedAgentId, assignedAgentId,
executionMode,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
})); }));
@@ -11240,6 +11242,7 @@ describe("TaskExecutor messaging tools", () => {
currentStep: 0, currentStep: 0,
log: [], log: [],
assignedAgentId, assignedAgentId,
executionMode,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}); });
@@ -11327,6 +11330,48 @@ describe("TaskExecutor messaging tools", () => {
expect(toolNames).not.toContain("list_agents"); expect(toolNames).not.toContain("list_agents");
expect(toolNames).not.toContain("delegate_task"); expect(toolNames).not.toContain("delegate_task");
}); });
describe("fast mode", () => {
beforeEach(() => {
vi.clearAllMocks();
mockedExistsSync.mockReturnValue(true);
});
it("excludes review_step tool when executionMode is 'fast'", async () => {
const tools = await captureCustomTools({
executionMode: "fast",
});
const toolNames = tools.map((t: any) => t.name);
expect(toolNames).not.toContain("review_step");
});
it("includes task_update and task_done tools in fast mode", async () => {
const tools = await captureCustomTools({
executionMode: "fast",
});
const toolNames = tools.map((t: any) => t.name);
expect(toolNames).toContain("task_update");
expect(toolNames).toContain("task_done");
});
it("includes review_step tool when executionMode is 'standard'", async () => {
const tools = await captureCustomTools({
executionMode: "standard",
});
const toolNames = tools.map((t: any) => t.name);
expect(toolNames).toContain("review_step");
});
it("includes review_step tool when executionMode is undefined (defaults to standard)", async () => {
const tools = await captureCustomTools({});
const toolNames = tools.map((t: any) => t.name);
expect(toolNames).toContain("review_step");
});
});
}); });
describe("determineRevisionResetStart", () => { describe("determineRevisionResetStart", () => {

View File

@@ -884,13 +884,17 @@ export class TaskExecutor {
executorLog.log(`${task.id}: recovered ${modifiedFiles.length} modified files`); executorLog.log(`${task.id}: recovered ${modifiedFiles.length} modified files`);
} }
// Run workflow steps before transitioning // Run workflow steps before transitioning — skip in fast mode
const workflowResult = await this.runWorkflowSteps(task, task.worktree, settings); if (task.executionMode !== "fast") {
if (!workflowResult.allPassed) { const workflowResult = await this.runWorkflowSteps(task, task.worktree, settings);
// For recovery path, treat any failure (including revision) as hard failure if (!workflowResult.allPassed) {
// Send back to in-progress so executor can attempt to fix the issues // For recovery path, treat any failure (including revision) as hard failure
await this.sendTaskBackForFix(task, task.worktree!, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed during recovery"); // Send back to in-progress so executor can attempt to fix the issues
return true; // Still transitioned out of in-progress await this.sendTaskBackForFix(task, task.worktree!, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed during recovery");
return true; // Still transitioned out of in-progress
}
} else {
executorLog.log(`${task.id}: fast mode — skipping workflow steps on auto-recovery`);
} }
} }
@@ -1100,6 +1104,9 @@ export class TaskExecutor {
// Fetch settings early — needed for worktree naming and later configuration // Fetch settings early — needed for worktree naming and later configuration
const settings = await this.store.getSettings(); const settings = await this.store.getSettings();
// Read execution mode to determine whether to skip review and workflow steps
const executionMode = task.executionMode ?? "standard";
// Construct run context for mutation correlation // Construct run context for mutation correlation
// Use a synthetic correlation ID: task ID + timestamp + random suffix // Use a synthetic correlation ID: task ID + timestamp + random suffix
const syntheticRunId = generateSyntheticRunId("exec", task.id); const syntheticRunId = generateSyntheticRunId("exec", task.id);
@@ -1472,21 +1479,27 @@ export class TaskExecutor {
await audit.filesystem({ type: "file:capture-modified", target: task.id, metadata: { files: modifiedFiles } }); await audit.filesystem({ type: "file:capture-modified", target: task.id, metadata: { files: modifiedFiles } });
} }
const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings); // Run workflow steps before moving to in-review — skip in fast mode
if (!workflowResult.allPassed) { if (executionMode !== "fast") {
// Check if revision was requested const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
if (workflowResult.revisionRequested) { if (!workflowResult.allPassed) {
await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName); // Check if revision was requested
if (workflowResult.revisionRequested) {
await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
return;
}
// Try to fix workflow step failures with retries
const retried = await this.handleWorkflowStepFailure(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown");
if (retried) {
return; // Retry scheduled
}
// Retries exhausted - send back to in-progress for remediation
await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed");
return; return;
} }
// Try to fix workflow step failures with retries } else {
const retried = await this.handleWorkflowStepFailure(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown"); executorLog.log(`${task.id}: fast mode — skipping pre-merge workflow steps`);
if (retried) { await this.store.logEntry(task.id, "Fast mode — pre-merge workflow steps skipped", undefined, this.currentRunContext);
return; // Retry scheduled
}
// Retries exhausted - send back to in-progress for remediation
await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed");
return;
} }
// Reset retry counters on success // Reset retry counters on success
@@ -1654,13 +1667,21 @@ export class TaskExecutor {
? await this.options.agentStore.getAgent(assignedAgentId).catch(() => null) ? await this.options.agentStore.getAgent(assignedAgentId).catch(() => null)
: null; : null;
// Log fast mode status
if (executionMode === "fast") {
executorLog.log(`${task.id}: fast mode — review_step tool not injected`);
}
const customTools = [ const customTools = [
this.createTaskUpdateTool(task.id, codeReviewVerdicts, sessionRef, stepCheckpoints, stuckDetector), this.createTaskUpdateTool(task.id, codeReviewVerdicts, sessionRef, stepCheckpoints, stuckDetector),
this.createTaskLogTool(task.id), this.createTaskLogTool(task.id),
this.createTaskCreateTool(), this.createTaskCreateTool(),
this.createTaskAddDepTool(task.id), this.createTaskAddDepTool(task.id),
this.createTaskDoneTool(task.id, () => { taskDone = true; }), this.createTaskDoneTool(task.id, () => { taskDone = true; }),
this.createReviewStepTool(task.id, worktreePath, detail.prompt, codeReviewVerdicts, sessionRef, stepCheckpoints, detail, stuckDetector), // Skip review_step tool in fast mode — fast mode bypasses automated review gates
...(executionMode !== "fast" ? [
this.createReviewStepTool(task.id, worktreePath, detail.prompt, codeReviewVerdicts, sessionRef, stepCheckpoints, detail, stuckDetector),
] : []),
this.createSpawnAgentTool(task.id, worktreePath, settings), this.createSpawnAgentTool(task.id, worktreePath, settings),
this.createTaskDocumentWriteTool(task.id), this.createTaskDocumentWriteTool(task.id),
this.createTaskDocumentReadTool(task.id), this.createTaskDocumentReadTool(task.id),
@@ -1941,22 +1962,27 @@ export class TaskExecutor {
executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`); executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
} }
// Run workflow steps before moving to in-review // Run workflow steps before moving to in-review — skip in fast mode
const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings); if (executionMode !== "fast") {
if (!workflowResult.allPassed) { const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
// Check if revision was requested if (!workflowResult.allPassed) {
if (workflowResult.revisionRequested) { // Check if revision was requested
await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName); if (workflowResult.revisionRequested) {
await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
return;
}
// Try to fix workflow step failures with retries
const retried = await this.handleWorkflowStepFailure(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown");
if (retried) {
return; // Retry scheduled
}
// Retries exhausted - send back to in-progress for remediation
await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed");
return; return;
} }
// Try to fix workflow step failures with retries } else {
const retried = await this.handleWorkflowStepFailure(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown"); executorLog.log(`${task.id}: fast mode — skipping pre-merge workflow steps`);
if (retried) { await this.store.logEntry(task.id, "Fast mode — pre-merge workflow steps skipped", undefined, this.currentRunContext);
return; // Retry scheduled
}
// Retries exhausted - send back to in-progress for remediation
await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed");
return;
} }
// Reset retry counters on success // Reset retry counters on success
@@ -2047,16 +2073,22 @@ export class TaskExecutor {
executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`); executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
} }
const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings); // Run workflow steps before moving to in-review — skip in fast mode
if (!workflowResult.allPassed) { if (executionMode !== "fast") {
// Check if revision was requested const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
if (workflowResult.revisionRequested) { if (!workflowResult.allPassed) {
await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName); // Check if revision was requested
if (workflowResult.revisionRequested) {
await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
return;
}
// Hard failure - send back to in-progress for remediation
await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed on retry");
return; return;
} }
// Hard failure - send back to in-progress for remediation } else {
await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed on retry"); executorLog.log(`${task.id}: fast mode — skipping pre-merge workflow steps`);
return; await this.store.logEntry(task.id, "Fast mode — pre-merge workflow steps skipped", undefined, this.currentRunContext);
} }
// Reset retry counters on success // Reset retry counters on success