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:
gsxdsm
2026-04-09 21:06:19 -07:00
parent 00e04c2d05
commit 34c11a7078
16 changed files with 476 additions and 19 deletions

View File

@@ -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));
}