feat(FN-1499): add workflow step revision loop signaling

- Add WorkflowStepSignal type with 'needs-revision' support for iterative QA loops
- Extend executeWorkflowStep() to detect needs-revision signals and trigger revision sessions
- Add revision loop limit (5 iterations max) to prevent infinite loops
- Track revisionAttempts in workflow step results
- Add comprehensive tests for revision signaling, loop behavior, and limit enforcement
- Document workflow step revision behavior in docs/workflow-steps.md
This commit is contained in:
gsxdsm
2026-04-10 10:50:11 -07:00
parent 5df39079cf
commit 0a102b5e29
5 changed files with 522 additions and 19 deletions

View File

@@ -3,6 +3,17 @@
## Architecture
- `TaskExecutor` terminates active agent sessions (single and step) when tasks are moved away from `in-progress` via the `task:moved` event handler. This prevents zombie sessions when users manually send tasks back to todo/triage from the board UI.
- **Workflow Step Revision Loop (FN-1499)**: Workflow steps can request implementation revisions via "REQUEST REVISION" output. The flow:
1. Workflow step agent outputs "REQUEST REVISION\n\n[feedback]" to signal that code changes are needed
2. `executeWorkflowStep()` detects this pattern and returns `WorkflowStepOutcome` with `revisionRequested: true`
3. `runWorkflowSteps()` propagates the structured outcome with `WorkflowStepResult.revisionRequested`
4. `handleWorkflowRevisionRequest()` is called, which:
- Injects "Workflow Revision Instructions" section into `PROMPT.md` (replacing any prior revision block)
- Resets all steps to `pending` for fresh execution
- Clears session file to get a fresh agent session
- Schedules fresh execution via `setTimeout` after current guard unwinds
5. Task stays in `in-progress` and the scheduler re-dispatches the task for a fresh executor pass
6. **Guard-unwind requirement**: The revision rerun MUST be scheduled after the current `execute()` guard clears (`this.executing.delete()`). Failure to observe this causes a race where the scheduler re-dispatches while the old execution guard is still set, silently no-oping the new dispatch and stranding the task in `in-progress` with no active session.
- **Review Handoff (FN-1259)**: Agents can hand off tasks to users for human review via steering comments containing handoff phrases ("send it back to me", "hand off to user", etc.). When `reviewHandoffPolicy` is `"comment-triggered"`, the executor detects handoff intent in agent-authored steering comments and executes the handoff: sets `status: "awaiting-user-review"`, `assigneeUserId: "requesting-user"`, moves task to `in-review`, and disposes the agent session. The merger skips tasks with `"awaiting-user-review"` status (via `BLOCKING_TASK_STATUSES` in `task-merge.ts`). Users can accept review (clear status) or return to agent (move to todo). The `assigneeUserId` field stores the user ID who should review the task.
- Agent preset templates in `NewAgentDialog.tsx` are a UI-only concept (`AgentPreset` interface), separate from the engine's `AgentPromptTemplate` type. Presets populate agent creation fields (name, icon, role, soul, instructionsText) but don't map to engine types.
- `soul` and `instructionsText` are already supported in `AgentCreateInput` and `AgentUpdateInput` — no API changes needed when adding these to presets.

View File

@@ -541,6 +541,8 @@ Edit mode never auto-injects workflow steps — existing task selections are pre
4. Post-merge steps run automatically after merge — results appear in the Workflow tab
5. View results in the task detail modal's **Workflow** tab
**Revision Loop:** If a pre-merge step identifies issues that need code changes, it can request a revision. The executor generates "Workflow Revision Instructions" in the task's PROMPT.md, resets all steps, and routes the task back to in-progress for a fresh implementation pass. Hard failures (e.g., broken test infrastructure) still move the task to in-review with a failed status.
## Architecture
### Storage

View File

@@ -59,6 +59,51 @@ Workflow step definitions support `defaultOn`.
When `defaultOn: true`, the step is preselected automatically for newly created tasks (users can still deselect it).
## Workflow Step Revision Loop
Workflow steps can request implementation revisions instead of just blocking completion.
### How It Works
When a prompt-mode workflow step agent finishes its review, it can output a **revision request** to indicate that code changes are needed:
```
REQUEST REVISION
Fix the SQL injection vulnerability in src/auth.ts. The login function does not
handle the case where the user account is locked.
```
### Behavior
When a revision is requested:
1. The executor generates a **Workflow Revision Instructions** section in the task's `PROMPT.md`
2. All step statuses are reset to `pending` for a fresh execution pass
3. The task remains in `in-progress` and a fresh executor session is scheduled
4. The agent receives the revision feedback at the start of its next session
### Feedback Format
Workflow step prompts should instruct agents to use this exact format for revision requests:
```
REQUEST REVISION
[Clear, actionable description of what needs to be fixed]
```
The revision block replaces any prior revision instructions (no accumulation).
### Hard Failures vs Revisions
Not all workflow failures are revision requests:
- **Revision requested**: Implementation needs changes → routes back to executor
- **Hard failure**: Unrecoverable issue → moves to `in-review` with `failed` status
Use revision requests when the implementation is wrong but fixable. Use hard failures only for genuine blockers (e.g., required files are missing, test infrastructure is broken).
## Viewing Results
Task detail modal includes a **Workflow** tab when workflow data exists.

View File

@@ -7138,6 +7138,220 @@ describe("Workflow Steps Execution", () => {
// Task should still move to in-review
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-review");
});
// ── Workflow Step Revision Request ──────────────────────────────────
it("workflow step agent returns revisionRequested when output starts with REQUEST REVISION", async () => {
const store = createMockStore();
store.getTask.mockResolvedValue({
id: "FN-001",
title: "Test",
description: "Test task",
column: "in-progress",
dependencies: [],
steps: [{ name: "Preflight", status: "pending" }],
currentStep: 0,
log: [],
enabledWorkflowSteps: ["WS-001"],
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
store.getWorkflowStep.mockResolvedValue({
id: "WS-001",
name: "Security Audit",
description: "Check for vulnerabilities",
prompt: "Scan for security issues.",
enabled: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// First call: main agent with task_done
// Second call: workflow step agent that returns REQUEST REVISION
let callIdx = 0;
let subscribeHandler: any;
mockedCreateHaiAgent.mockImplementation((async (opts: any) => {
callIdx++;
if (callIdx === 1) {
// Main execution
const customTools = opts.customTools || [];
const session = {
prompt: vi.fn().mockImplementation(async () => {
const taskDoneTool = customTools.find((t: any) => t.name === "task_done");
if (taskDoneTool) await taskDoneTool.execute("tool-1", {});
}),
dispose: vi.fn(),
subscribe: vi.fn(),
on: vi.fn(),
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
state: {},
};
return { session };
} else {
// Workflow step agent that requests revision
const session = {
prompt: vi.fn().mockImplementation(async () => {
// Call the subscribe handler to simulate agent outputting REQUEST REVISION
if (subscribeHandler) {
subscribeHandler({
type: "message_update",
assistantMessageEvent: { type: "text_delta", delta: "REQUEST REVISION\n\nFix the SQL injection vulnerability in src/auth.ts" },
});
}
}),
dispose: vi.fn(),
subscribe: vi.fn((handler: any) => {
subscribeHandler = handler;
}),
state: {},
};
return { session };
}
}) as any);
const onComplete = vi.fn();
const executor = new TaskExecutor(store, "/tmp/test", { onComplete });
await executor.execute({
id: "FN-001",
title: "Test",
description: "Test task",
column: "in-progress",
dependencies: [],
steps: [{ name: "Preflight", status: "pending" }],
currentStep: 0,
log: [],
enabledWorkflowSteps: ["WS-001"],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Both agents should be called
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2);
// Log should show revision was requested
expect(store.logEntry).toHaveBeenCalledWith(
"FN-001",
expect.stringContaining("requested revision"),
expect.stringContaining("SQL injection"),
);
// Workflow step result should be marked as failed
expect(store.updateTask).toHaveBeenCalledWith(
"FN-001",
expect.objectContaining({
workflowStepResults: expect.arrayContaining([
expect.objectContaining({
workflowStepId: "WS-001",
status: "failed",
output: expect.stringContaining("SQL injection"),
}),
]),
}),
);
// Task should NOT be marked as failed (revision requested, not hard failure)
expect(store.updateTask).not.toHaveBeenCalledWith(
"FN-001",
expect.objectContaining({ status: "failed", error: "Workflow step failed" }),
);
});
it("hard failure workflow step moves task to in-review with failed status", async () => {
const store = createMockStore();
store.getTask.mockResolvedValue({
id: "FN-001",
title: "Test",
description: "Test task",
column: "in-progress",
dependencies: [],
steps: [{ name: "Preflight", status: "pending" }],
currentStep: 0,
log: [],
enabledWorkflowSteps: ["WS-001"],
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
store.getWorkflowStep.mockResolvedValue({
id: "WS-001",
name: "QA Check",
description: "Run tests",
prompt: "Run the test suite.",
enabled: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
let callIdx = 0;
mockedCreateHaiAgent.mockImplementation((async (opts: any) => {
callIdx++;
if (callIdx === 1) {
const customTools = opts.customTools || [];
const session = {
prompt: vi.fn().mockImplementation(async () => {
const taskDoneTool = customTools.find((t: any) => t.name === "task_done");
if (taskDoneTool) await taskDoneTool.execute("tool-1", {});
}),
dispose: vi.fn(),
subscribe: vi.fn(),
on: vi.fn(),
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
state: {},
};
return { session };
} else {
// Workflow step agent that passes (no REQUEST REVISION)
return {
session: {
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
subscribe: vi.fn(),
state: {},
},
};
}
}) as any);
const onComplete = vi.fn();
const onError = vi.fn();
const executor = new TaskExecutor(store, "/tmp/test", { onComplete, onError });
await executor.execute({
id: "FN-001",
title: "Test",
description: "Test task",
column: "in-progress",
dependencies: [],
steps: [{ name: "Preflight", status: "pending" }],
currentStep: 0,
log: [],
enabledWorkflowSteps: ["WS-001"],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
// Both agents called
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2);
// Log should show workflow step passed
expect(store.logEntry).toHaveBeenCalledWith(
"FN-001",
expect.stringContaining("Workflow step completed"),
);
// Task should move to in-review (not revision loop)
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-review");
// onComplete should be called
expect(onComplete).toHaveBeenCalled();
expect(onError).not.toHaveBeenCalled();
});
});
describe("Real-time steering injection", () => {

View File

@@ -1,6 +1,7 @@
import { execSync } from "node:child_process";
import { join } from "node:path";
import { existsSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import type { TaskStore, Task, TaskDetail, StepStatus, Settings, WorkflowStep, MissionStore, Slice, AgentState, AgentCapability, RunMutationContext } from "@fusion/core";
import type { AgentStore } from "@fusion/core";
import { buildExecutionMemoryInstructions, getTaskMergeBlocker, resolveAgentPrompt } from "@fusion/core";
@@ -88,6 +89,27 @@ interface SpawnAgentResult {
message: string;
}
/**
* Outcome of a single workflow step execution.
* Supports three states: pass, hard failure, or revision requested with feedback.
*/
export interface WorkflowStepOutcome {
success: boolean;
revisionRequested?: boolean;
output?: string;
error?: string;
}
/**
* Result of running all pre-merge workflow steps.
* Returns true if all passed, false if any hard failure, or a structured
* revision result if a revision was requested.
*/
export type WorkflowStepResult =
| { allPassed: true }
| { allPassed: false; revisionRequested: false }
| { allPassed: false; revisionRequested: true; feedback: string; stepName: string };
const reviewStepParams = Type.Object({
step: Type.Number({ description: "Step number to review" }),
@@ -656,8 +678,9 @@ export class TaskExecutor {
}
// Run workflow steps before transitioning
const workflowSuccess = await this.runWorkflowSteps(task, task.worktree, settings);
if (!workflowSuccess) {
const workflowResult = await this.runWorkflowSteps(task, task.worktree, settings);
if (!workflowResult.allPassed) {
// For recovery path, treat any failure (including revision) as hard failure
await this.store.updateTask(task.id, { status: "failed", error: "Workflow step failed during recovery" });
await this.store.moveTask(task.id, "in-review");
executorLog.log(`${task.id} workflow step failed during recovery → in-review`);
@@ -1082,8 +1105,14 @@ export class TaskExecutor {
await audit.filesystem({ type: "file:capture-modified", target: task.id, metadata: { files: modifiedFiles } });
}
const workflowSuccess = await this.runWorkflowSteps(task, worktreePath, settings);
if (!workflowSuccess) {
const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
if (!workflowResult.allPassed) {
// Check if revision was requested
if (workflowResult.revisionRequested) {
await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
return;
}
// Hard failure - move to in-review
await this.store.updateTask(task.id, { status: "failed", error: "Workflow step failed" });
await this.store.moveTask(task.id, "in-review");
// Audit trail: record task move (FN-1404)
@@ -1493,9 +1522,14 @@ export class TaskExecutor {
}
// Run workflow steps before moving to in-review
const workflowSuccess = await this.runWorkflowSteps(task, worktreePath, settings);
if (!workflowSuccess) {
// Move to in-review even when workflow steps fail so users can see the failure
const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
if (!workflowResult.allPassed) {
// Check if revision was requested
if (workflowResult.revisionRequested) {
await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
return;
}
// Hard failure - move to in-review so users can see the failure
await this.store.updateTask(task.id, { status: "failed", error: "Workflow step failed" });
await this.store.moveTask(task.id, "in-review");
executorLog.log(`${task.id} workflow step failed → in-review`);
@@ -1583,8 +1617,14 @@ export class TaskExecutor {
executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
}
const workflowSuccess = await this.runWorkflowSteps(task, worktreePath, settings);
if (!workflowSuccess) {
const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
if (!workflowResult.allPassed) {
// Check if revision was requested
if (workflowResult.revisionRequested) {
await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
return;
}
// Hard failure
await this.store.updateTask(task.id, { status: "failed", error: "Workflow step failed" });
await this.store.moveTask(task.id, "in-review");
executorLog.log(`${task.id} workflow step failed on retry → in-review`);
@@ -2264,6 +2304,133 @@ export class TaskExecutor {
await this.store.logEntry(taskId, "Execution stopped — work discarded, moved to triage for re-specification");
}
/**
* Handle a workflow step revision request.
*
* This method:
* 1. Updates PROMPT.md with "Workflow Revision Instructions" section
* 2. Resets task execution state (all steps reset to pending)
* 3. Schedules fresh execution to run after current guard unwinds
*
* The task stays in "in-progress" and is scheduled for a fresh executor pass.
*/
private async handleWorkflowRevisionRequest(
task: Task,
worktreePath: string,
feedback: string,
stepName: string,
): Promise<void> {
executorLog.log(`${task.id}: workflow revision requested by step "${stepName}"`);
await this.store.logEntry(
task.id,
`Workflow step "${stepName}" requested revision — resetting execution state`,
feedback,
);
// 1. Update PROMPT.md with revision instructions
await this.injectWorkflowRevisionInstructions(task, feedback);
// 2. Reset all steps to pending for fresh execution
const updatedTask = await this.store.getTask(task.id);
for (let i = 0; i < updatedTask.steps.length; i++) {
if (updatedTask.steps[i].status !== "pending") {
await this.store.updateStep(task.id, i, "pending");
}
}
// 3. Clear any session file so we get a fresh session
await this.store.updateTask(task.id, {
status: null,
sessionFile: null,
});
// 4. Schedule fresh execution after guard unwinds
// This prevents the race condition where the scheduler re-dispatches
// while the old execution guard is still set.
executorLog.log(`${task.id}: scheduling fresh execution after revision request`);
setTimeout(async () => {
try {
// Move task to todo briefly, then back to in-progress to trigger fresh execution
// The task is already in in-progress, so we need to:
// 1. Move to todo (this triggers the guard to clear)
// 2. Move back to in-progress (this triggers fresh execution)
await this.store.moveTask(task.id, "todo");
await this.store.moveTask(task.id, "in-progress");
executorLog.log(`${task.id}: revision rerun scheduled — moved to todo then in-progress`);
} catch (err: any) {
executorLog.error(`${task.id}: failed to schedule revision rerun: ${err.message}`);
// Fallback: log entry and let scheduler pick it up on next tick
await this.store.logEntry(
task.id,
"Workflow revision requested — executor ready for fresh execution",
);
}
}, 0);
}
/**
* Inject or update the "Workflow Revision Instructions" section in PROMPT.md.
* This section contains feedback from workflow steps that requested revisions.
* The section is replaced entirely to avoid accumulation of old feedback.
*/
private async injectWorkflowRevisionInstructions(task: Task, feedback: string): Promise<void> {
const promptPath = join(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
// Read existing PROMPT.md
let content: string;
try {
content = await readFile(promptPath, "utf-8");
} catch {
executorLog.warn(`${task.id}: PROMPT.md not found at ${promptPath}, skipping revision injection`);
return;
}
// Check for existing Workflow Revision Instructions section
const revisionSectionHeader = "## Workflow Revision Instructions";
const revisionSectionContent = `${revisionSectionHeader}
The following feedback was received from quality gates and requires implementation changes:
${feedback}
**Important:** This is a revision request — address the feedback above by making the necessary code changes, then mark all affected steps as done and call task_done() when complete.
`;
let newContent: string;
if (content.includes(revisionSectionHeader)) {
// Replace existing section
const sectionRegex = new RegExp(
`${revisionSectionHeader}[\\s\\S]*?(?=\\n## |\\n# |$)`,
"i"
);
if (sectionRegex.test(content)) {
newContent = content.replace(sectionRegex, revisionSectionContent);
} else {
// Fallback: append at end
newContent = content + "\n" + revisionSectionContent;
}
} else {
// Append new section before any closing markers or at end
// Look for common markers like "## Acceptance Criteria" or just append
const acceptanceCriteriaMatch = content.match(/\n##\s+Acceptance Criteria\n/);
if (acceptanceCriteriaMatch) {
const insertIdx = acceptanceCriteriaMatch.index!;
newContent = content.slice(0, insertIdx) + "\n" + revisionSectionContent + content.slice(insertIdx);
} else {
newContent = content + "\n" + revisionSectionContent;
}
}
// Write updated content
try {
await writeFile(promptPath, newContent);
executorLog.log(`${task.id}: injected workflow revision instructions into PROMPT.md`);
} catch (err: any) {
executorLog.error(`${task.id}: failed to inject revision instructions: ${err.message}`);
}
}
/**
* Capture the list of files modified during agent execution.
* Uses git diff against the stored baseCommitSha to determine what changed.
@@ -2332,16 +2499,16 @@ export class TaskExecutor {
/**
* Run workflow step agents sequentially after main task execution completes.
* Each workflow step spawns a separate agent with the step's prompt.
* Returns true if all steps pass, false if any fails.
* Returns structured result: all passed, all passed (true), failed (false), or revision requested.
*/
private async runWorkflowSteps(
task: Task,
worktreePath: string,
settings: Settings,
): Promise<boolean> {
): Promise<WorkflowStepResult> {
// Check if task has enabled workflow steps
const currentTask = await this.store.getTask(task.id);
if (!currentTask.enabledWorkflowSteps?.length) return true;
if (!currentTask.enabledWorkflowSteps?.length) return { allPassed: true };
const workflowStepIds = currentTask.enabledWorkflowSteps;
const results: import("@fusion/core").WorkflowStepResult[] = [];
@@ -2403,7 +2570,7 @@ export class TaskExecutor {
const startedAt = new Date().toISOString();
try {
const result = stepMode === "script"
const result: WorkflowStepOutcome = stepMode === "script"
? await this.executeScriptWorkflowStep(task, ws, worktreePath, settings)
: await this.executeWorkflowStep(task, ws, worktreePath, settings);
const completedAt = new Date().toISOString();
@@ -2421,7 +2588,32 @@ export class TaskExecutor {
completedAt,
});
await this.store.updateTask(task.id, { workflowStepResults: results });
} else if (result.revisionRequested) {
// Revision requested — this is a structured outcome that routes back to executor
await this.store.logEntry(
task.id,
`[pre-merge] Workflow step requested revision: ${ws.name}`,
result.output,
);
executorLog.log(`${task.id} — [pre-merge] workflow step requested revision: ${ws.name}`);
results.push({
workflowStepId: ws.id,
workflowStepName: ws.name,
phase: stepPhase,
status: "failed",
output: result.output || "Revision requested",
startedAt,
completedAt,
});
await this.store.updateTask(task.id, { workflowStepResults: results });
return {
allPassed: false,
revisionRequested: true,
feedback: result.output || "Workflow step requested revision",
stepName: ws.name,
};
} else {
// Hard failure
await this.store.logEntry(
task.id,
`[pre-merge] Workflow step failed: ${ws.name}`,
@@ -2438,7 +2630,7 @@ export class TaskExecutor {
completedAt,
});
await this.store.updateTask(task.id, { workflowStepResults: results });
return false;
return { allPassed: false, revisionRequested: false };
}
} catch (err: any) {
const completedAt = new Date().toISOString();
@@ -2458,11 +2650,11 @@ export class TaskExecutor {
completedAt,
});
await this.store.updateTask(task.id, { workflowStepResults: results });
return false;
return { allPassed: false, revisionRequested: false };
}
}
return true;
return { allPassed: true };
}
/**
@@ -2512,13 +2704,14 @@ export class TaskExecutor {
/**
* Execute a single workflow step by spawning an agent with the step's prompt.
* Returns structured outcome with support for revision requests.
*/
private async executeWorkflowStep(
task: Task,
workflowStep: WorkflowStep,
worktreePath: string,
settings: Settings,
): Promise<{ success: boolean; output?: string; error?: string }> {
): Promise<WorkflowStepOutcome> {
const toolMode: "coding" | "readonly" = workflowStep.toolMode || "readonly";
const systemPrompt = `You are a workflow step agent executing: ${workflowStep.name}
@@ -2531,8 +2724,32 @@ Your Instructions:
${workflowStep.prompt}
You have access to the file system to review changes.
When your review is complete and everything looks good, simply state your findings.
If issues are found that need attention, describe them clearly.`;
## Feedback Format
When your review is complete, you MUST use one of these exact formats:
**For PASS (no issues found):**
Simply state your findings and approval. No special formatting required.
**For REVISION REQUESTED (issues found that require code changes):**
Your response MUST start with the exact phrase:
\`REQUEST REVISION\`
Followed by a clear, actionable description of what needs to be fixed.
Be specific: reference exact files, line numbers, or functions that need changes.
Example:
\`REQUEST REVISION
The login function in src/auth.ts does not handle the case where the user
account is locked. Add proper error handling for the LOCKED_ACCOUNT error code
and show an appropriate message to the user.\`
**Important:**
- Only use "REQUEST REVISION" when the implementation needs code changes.
- If the code is correct and no changes are needed, just state your findings.
- Be constructive and actionable — vague feedback wastes the executor's time.`;
const agentLogger = new AgentLogger({
store: this.store,
@@ -2599,6 +2816,20 @@ If issues are found that need attention, describe them clearly.`;
session.dispose();
await agentLogger.flush();
// Check if the output contains a revision request
const trimmedOutput = output.trim();
const revisionMatch = trimmedOutput.match(/^REQUEST REVISION\s*\n*/i);
if (revisionMatch) {
// Extract the feedback after "REQUEST REVISION"
const feedbackStart = revisionMatch[0].length;
const feedback = trimmedOutput.slice(feedbackStart).trim();
return {
success: false,
revisionRequested: true,
output: feedback,
};
}
return { success: true, output };
} catch (err: any) {
await agentLogger.flush();