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:
@@ -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", () => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user