using Nexus.Api.Data; using Nexus.Api.DTOs; using Nexus.Api.Models; using Nexus.Api.Repositories; namespace Nexus.Api.Services; /// /// Concrete implementation of ITaskBridgeService. /// Wraps ITaskService, IActivityRepository, INotificationService, and ILiveUpdateService /// into structured, predictable commands for agent-facing usage. /// /// All operations produce typed TaskBridgeResult with explicit error codes, /// making agent consumption safe and debuggable. /// public sealed class TaskBridgeService( ITaskService taskService, IActivityRepository activityRepo, INotificationService notificationService, ILiveUpdateService liveUpdateService) : ITaskBridgeService { private static readonly HashSet ValidStates = new(TaskStateHelper.AllStates, StringComparer.OrdinalIgnoreCase); // ──────────────────────────────── Create Task ──────────────────────────────── public async Task> CreateTaskAsync( string title, string? detail = null, string? source = "iris", string? priority = "Normal", string? assignedTo = null, Guid? projectId = null, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(title)) return Error(TaskBridgeOutcome.ValidationError, "Title is required."); var normalizedSource = NormalizeSource(source); var normalizedAssignee = NormalizeAssignedTo(assignedTo); var task = await taskService.CreateDashboardTaskAsync( title.Trim(), detail?.Trim(), normalizedSource, priority, normalizedAssignee, parentTaskId: null, ct); var dto = MapToDto(task); return Success(dto); } // ──────────────────────────────── Create Child Task ────────────────────────── public async Task> CreateChildTaskAsync( Guid parentTaskId, string title, string? detail = null, string? source = "iris", string? priority = "Normal", string? assignedTo = null, string? expectedFrom = null, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(title)) return Error(TaskBridgeOutcome.ValidationError, "Title is required."); // Verify parent exists var parent = await taskService.GetByIdAsync(parentTaskId, ct); if (parent is null) return Error(TaskBridgeOutcome.NotFound, $"Parent task {parentTaskId} not found."); var normalizedAssignee = NormalizeAssignedTo(assignedTo); var task = await taskService.CreateAgentTaskAsync( title.Trim(), detail?.Trim(), NormalizeSource(source), priority, normalizedAssignee, expectedFrom, parentTaskId, ct); // If parent was in Backlog, move it to InProgress (coordination starts) if (string.Equals(parent.State, "Backlog", StringComparison.OrdinalIgnoreCase)) { await taskService.UpdateStatusAsync(parentTaskId, "In progress", ct); } var dto = MapToDto(task); return Success(dto); } // ──────────────────────────────── Update Status ────────────────────────────── public async Task> UpdateStatusAsync( Guid taskId, string state, string? callerAgent = null, CancellationToken ct = default) { if (!ValidStates.Contains(state)) return Error(TaskBridgeOutcome.ValidationError, $"Invalid state '{state}'. Valid: {string.Join(", ", TaskStateHelper.AllStates)}"); var task = await taskService.GetByIdAsync(taskId, ct); if (task is null) return Error(TaskBridgeOutcome.NotFound, $"Task {taskId} not found."); // Check authorization if (!TaskStateHelper.CanChangeState(callerAgent, task)) return Error(TaskBridgeOutcome.Unauthorized, $"Agent '{callerAgent}' is not authorized to change task state. Only iris and bao may move tasks."); var result = await taskService.UpdateStatusAsync(taskId, state, ct); if (result.Outcome != TaskOperationOutcome.Success) return Error(TaskBridgeOutcome.InvalidState, "Status update rejected."); var dto = MapToDto(result.Task!); return Success(dto); } // ──────────────────────────────── Append Activity ──────────────────────────── public async Task> AppendActivityAsync( Guid taskId, string message, string? type = "comment", CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(message)) return Error(TaskBridgeOutcome.ValidationError, "Message is required."); var task = await taskService.GetByIdAsync(taskId, ct); if (task is null) return Error(TaskBridgeOutcome.NotFound, $"Task {taskId} not found."); var ev = new ActivityEvent { Type = type ?? "comment", Message = message.Trim(), TaskId = taskId }; await activityRepo.AddAsync(ev, ct); // Trigger live update so the board refreshes var board = await taskService.GetBoardAsync(ct); liveUpdateService.Publish("tasks.board.snapshot", board); return Success(ev); } // ──────────────────────────────── Handoff ──────────────────────────────────── public async Task> HandoffAsync( Guid taskId, string targetAgent, string? note = null, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(targetAgent)) return Error(TaskBridgeOutcome.ValidationError, "Target agent is required."); var task = await taskService.GetByIdAsync(taskId, ct); if (task is null) return Error(TaskBridgeOutcome.NotFound, $"Task {taskId} not found."); var normalizedTarget = targetAgent.Trim().ToLowerInvariant(); var handoffNote = string.IsNullOrWhiteSpace(note) ? $"Handoff → {normalizedTarget}" : $"Handoff → {normalizedTarget}: {note.Trim()}"; // Update expected-from and optionally assigned-to task.ExpectedFrom = normalizedTarget; // If this is a child task (has parent), keep assigned-to on the child // If standalone, set assigned-to to the target if (!task.ParentTaskId.HasValue) task.AssignedTo = normalizedTarget; await taskService.UpdateDashboardTaskAsync( taskId, title: null, detail: null, source: null, priority: null, assignedTo: task.AssignedTo, dueDate: null, ct); // Append handoff activity await AppendActivityAsync(taskId, handoffNote, "handoff", ct); // Notify the target await notificationService.CreateAsync( "task_assigned", $"Handoff: {task.Title}", handoffNote, normalizedTarget, task.Id, ct); var dto = MapToDto(task); return Success(dto); } // ──────────────────────────────── Query ────────────────────────────────────── public async Task GetBoardAsync(CancellationToken ct = default) => await taskService.GetBoardAsync(ct); public async Task> GetTaskAsync( Guid taskId, CancellationToken ct = default) { var dto = await taskService.GetDashboardTaskByIdAsync(taskId, ct); return dto is null ? Error(TaskBridgeOutcome.NotFound, $"Task {taskId} not found.") : Success(dto); } public async Task> GetChildTasksAsync( Guid parentTaskId, CancellationToken ct = default) { var children = await taskService.GetChildTasksAsync(parentTaskId, ct); return children.Select(MapToDto).ToList(); } public async Task> GetTaskActivityAsync( Guid taskId, CancellationToken ct = default) => await taskService.GetTaskActivityAsync(taskId, ct); public async Task GetAgentOverviewAsync( TimeSpan? staleThreshold = null, CancellationToken ct = default) { var threshold = staleThreshold ?? TimeSpan.FromHours(2); return await taskService.GetAgentWorkflowOverviewAsync(threshold, ct); } // ──────────────────────────────── Helpers ──────────────────────────────────── private static TaskBridgeResult Success(T data) => new(TaskBridgeOutcome.Success, data); private static TaskBridgeResult Error(TaskBridgeOutcome outcome, string error) => new(outcome, Data: default, Error: error); private static string NormalizeSource(string? source) => string.IsNullOrWhiteSpace(source) ? "iris" : source.Trim().ToLowerInvariant(); private static string? NormalizeAssignedTo(string? assignedTo) { if (string.IsNullOrWhiteSpace(assignedTo)) return null; var valid = new HashSet { "bao", "iris", "programmer", "reviewer", "architekt", "researcher", "executor" }; var lower = assignedTo.Trim().ToLowerInvariant(); return valid.Contains(lower) ? lower : null; } private static DashboardTaskDto MapToDto(WorkTask t) => new( t.Id, t.Title, t.Detail, t.Source, t.State, t.Priority, t.AssignedTo, t.ParentTaskId, t.DueDate, t.CreatedAt, t.UpdatedAt, t.IsAgentTask, t.ExpectedFrom); }