From 0bfbdec09b8ec3644bd253d2a5f27dec736ba80f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 27 Mar 2026 00:39:41 -0400 Subject: [PATCH] feat(07-02): implement sweepStaleReviews with escalation chain - sweepStaleReviews queries tasks in in_review status exceeding 30-min timeout - Escalation dispatched to reviewer's manager (manager's manager in chain) - When reviewer is CEO (no higher manager), desktop notification sent to human - Pending approvals auto-approved with documented timeout note - Task status advanced from in_review to done after escalation - Race condition with manual approval handled via try-catch (Pitfall 3) - Tasks without assigneeAgentId skipped gracefully - Exports REVIEW_TIMEOUT_MS (1800000ms) and SweepStaleReviewsDeps interface - 8 comprehensive unit tests covering all TASK-04 behaviors Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main/task-quality-gates.ts | 156 +++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/main/task-quality-gates.ts b/src/main/task-quality-gates.ts index 82bbc159..4e0e61d1 100644 --- a/src/main/task-quality-gates.ts +++ b/src/main/task-quality-gates.ts @@ -161,3 +161,159 @@ export function checkCapabilityMatch( warning: `Agent "${agent.name}" may lack skills for this task. Their capabilities (${agent.capabilities.slice(0, 80)}...) have no overlap with the task description. Consider reassigning.`, }; } + +// --------------------------------------------------------------------------- +// TASK-04: Stale Review Escalation +// --------------------------------------------------------------------------- + +export const REVIEW_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes (D-08) + +export interface SweepStaleReviewsDeps { + db: { + queryAll: (sql: string, ...params: unknown[]) => T[]; + getManagerForAgent: (agentId: string) => { id: string; name: string; companyId: string } | null; + getChainOfCommand: (agentId: string) => Array<{ id: string; name: string }>; + advanceTaskStatus: (taskId: string, newStatus: string, comment?: string) => { previousStatus: string }; + }; + dispatchAgentMessage: ( + deps: { + db: SweepStaleReviewsDeps["db"]; + emitEvent: (...args: unknown[]) => void; + publishDomainChanged: () => void; + wakeAgentIfPossible: (agentId: string, companyId: string, trigger: string) => void; + }, + message: { + companyId: string; + fromAgentId: string; + toAgentId: string | null; + channel: string; + subject: string; + body: string; + priority: string; + }, + wakeTrigger: string, + ) => { ok: boolean }; + resolveApprovalDecision: (params: { + db: SweepStaleReviewsDeps["db"]; + handleApprovalResolved: (approvalId: string, companyId: string, decision: string) => void; + companyId: string; + approvalId: string; + state: "approved" | "rejected" | "revision_requested"; + decisionNote?: string; + detail?: string | null; + actor?: string; + }) => void; + notify: (options: { + title: string; + body: string; + urgency?: string; + navigation?: { section: string; entityId?: string }; + batchKey?: string; + }) => void; + handleTaskStatusChange: (taskId: string, companyId: string, previousStatus: string, newStatus: string) => void; + handleApprovalResolved: (approvalId: string, companyId: string, decision: string) => void; + emitEvent: (...args: unknown[]) => void; + publishDomainChanged: () => void; + wakeAgentIfPossible: (agentId: string, companyId: string, trigger: string) => void; + logger: { info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void }; +} + +export function sweepStaleReviews( + companyId: string, + deps: SweepStaleReviewsDeps, + now: number = Date.now(), +): number { + let escalatedCount = 0; + const reviewTasks = deps.db.queryAll<{ + id: string; + assigneeAgentId: string | null; + updatedAt: string; + title: string; + }>( + `SELECT id, assignee_agent_id as assigneeAgentId, updated_at as updatedAt, title + FROM tasks + WHERE company_id = ? AND status = 'in_review'`, + companyId, + ); + + for (const task of reviewTasks) { + const reviewAge = now - new Date(task.updatedAt).getTime(); + if (reviewAge < REVIEW_TIMEOUT_MS) continue; + if (!task.assigneeAgentId) continue; + + // The reviewer is the assignee's direct manager + const reviewer = deps.db.getManagerForAgent(task.assigneeAgentId); + if (!reviewer) continue; // No manager = CEO-assigned task, skip escalation + + // D-09: Escalate to the reviewer's manager (manager's manager) + const chain = deps.db.getChainOfCommand(reviewer.id); + const escalationTarget = chain.length > 0 ? chain[0] : null; + + if (escalationTarget) { + deps.dispatchAgentMessage( + { + db: deps.db, + emitEvent: deps.emitEvent, + publishDomainChanged: deps.publishDomainChanged, + wakeAgentIfPossible: deps.wakeAgentIfPossible, + }, + { + companyId, + fromAgentId: reviewer.id, + toAgentId: escalationTarget.id, + channel: "direct", + subject: `Review timeout: ${task.title}`, + body: `Review by ${reviewer.name} on "${task.title}" has been pending for over 30 minutes. Auto-escalating.`, + priority: "urgent", + }, + "subtask_completed", + ); + } else { + // D-09: No higher manager (reviewer is CEO) -- notify human + deps.notify({ + title: "Review Timeout", + body: `"${task.title}" review has been pending over 30 minutes. Auto-approving.`, + urgency: "critical", + navigation: { section: "tasks", entityId: task.id }, + batchKey: "review_escalation", + }); + } + + // D-10: Auto-approve any pending approval linked to this task + const pendingApprovals = deps.db.queryAll<{ id: string }>( + `SELECT id FROM approvals + WHERE company_id = ? AND related_task_id = ? AND state = 'pending' + LIMIT 1`, + companyId, + task.id, + ); + for (const approval of pendingApprovals) { + try { + deps.resolveApprovalDecision({ + db: deps.db, + handleApprovalResolved: deps.handleApprovalResolved, + companyId, + approvalId: approval.id, + state: "approved", + decisionNote: "Auto-approved: review timeout exceeded 30 minutes.", + detail: "Auto-approved by review timeout escalation", + actor: "system", + }); + } catch { + // Already decided (race condition with manual approval) -- skip silently (Pitfall 3) + } + } + + // D-10: Advance task status from in_review to done + try { + deps.db.advanceTaskStatus(task.id, "done", "Auto-advanced: review timeout exceeded 30 minutes."); + deps.handleTaskStatusChange(task.id, companyId, "in_review", "done"); + } catch { + // Task may have been updated by manual action + } + + escalatedCount++; + } + + return escalatedCount; +}