feat(FN-1259): add review handoff mechanism for user assignment
- Add assigneeUserId field to Task type and SQLite schema for human assignment - Add reviewHandoffPolicy setting to control automatic handoff behavior - Implement handoff detection in executor: detect user assignment during review and auto-transition task - Add dashboard API routes for user assignment, handoff queries, and completion - Add frontend API functions: getHandoffTask, assignTaskToUser, completeHandoff - Add comprehensive tests for store methods, API routes, and executor handoff logic - Update memory documentation with review handoff pattern
This commit is contained in:
@@ -485,6 +485,20 @@ export class TaskExecutor {
|
||||
// the agent with failed injections. The error is logged for debugging.
|
||||
}
|
||||
}
|
||||
|
||||
// After injecting comments, check for review handoff intent
|
||||
// Only detect handoff in agent-authored comments when policy is enabled
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.reviewHandoffPolicy === "comment-triggered") {
|
||||
const agentComments = newComments.filter(c => c.author !== "user");
|
||||
for (const comment of agentComments) {
|
||||
if (detectReviewHandoffIntent(comment.text)) {
|
||||
executorLog.log(`Review handoff detected in ${task.id}: ${comment.text.slice(0, 50)}...`);
|
||||
await this.executeReviewHandoff(task, session, activeSession);
|
||||
return; // Exit early - handoff handles session disposal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -530,6 +544,59 @@ export class TaskExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a review handoff: move the task to in-review column with
|
||||
* awaiting-user-review status, assign the requesting user, and dispose
|
||||
* the agent session.
|
||||
*/
|
||||
private async executeReviewHandoff(
|
||||
task: Task,
|
||||
session: AgentSession,
|
||||
sessionEntry: { session: AgentSession; seenSteeringIds: Set<string>; lastModelProvider?: string | null; lastModelId?: string | null },
|
||||
): Promise<void> {
|
||||
try {
|
||||
executorLog.log(`Executing review handoff for ${task.id}`);
|
||||
|
||||
// Log the handoff event
|
||||
await this.store.logEntry(
|
||||
task.id,
|
||||
"Review handoff requested by agent — moving to in-review for user review",
|
||||
undefined,
|
||||
this.currentRunContext
|
||||
);
|
||||
|
||||
// Update task with awaiting-user-review status and assignee
|
||||
// Use a single updateTask call for atomicity
|
||||
await this.store.updateTask(
|
||||
task.id,
|
||||
{
|
||||
status: "awaiting-user-review",
|
||||
assigneeUserId: "requesting-user",
|
||||
},
|
||||
this.currentRunContext
|
||||
);
|
||||
|
||||
// Move the task to in-review column (this will also emit task:moved event)
|
||||
// The task:moved handler will clean up activeSessions
|
||||
await this.store.moveTask(task.id, "in-review");
|
||||
|
||||
// Dispose the agent session (this may already be done by task:moved handler)
|
||||
// but we do it here to be explicit
|
||||
if (this.activeSessions.has(task.id)) {
|
||||
const { session: activeSession } = this.activeSessions.get(task.id)!;
|
||||
activeSession.dispose();
|
||||
this.activeSessions.delete(task.id);
|
||||
}
|
||||
|
||||
// Untrack from stuck detector
|
||||
this.options.stuckTaskDetector?.untrackTask(task.id);
|
||||
|
||||
executorLog.log(`Review handoff complete for ${task.id} — task moved to in-review`);
|
||||
} catch (err: any) {
|
||||
executorLog.error(`Failed to execute review handoff for ${task.id}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast-path a completed task directly to in-review without spawning a new agent.
|
||||
* Captures modified files, runs workflow steps, and transitions the task.
|
||||
@@ -3396,3 +3463,23 @@ function formatCommentForInjection(comment: import("@fusion/core").SteeringComme
|
||||
const timestamp = formatTimestamp(comment.createdAt);
|
||||
return `📣 **New feedback** — ${timestamp} (${comment.author}):\n\n${comment.text}\n\nPlease adjust your approach based on this feedback.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if a steering comment contains a review handoff request.
|
||||
* Matches common handoff phrases that agents can use to request
|
||||
* human review of their work.
|
||||
*/
|
||||
export function detectReviewHandoffIntent(commentText: string): boolean {
|
||||
const text = commentText.toLowerCase();
|
||||
const handoffPhrases = [
|
||||
"send it back to me",
|
||||
"hand off to user",
|
||||
"needs human review",
|
||||
"assign to user",
|
||||
"return to user",
|
||||
"user review needed",
|
||||
"requesting user review",
|
||||
];
|
||||
|
||||
return handoffPhrases.some((phrase) => text.includes(phrase));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user