feat: complete task board workflow gates
This commit is contained in:
@@ -15,6 +15,7 @@ namespace Nexus.Api.Services;
|
||||
/// </summary>
|
||||
public sealed class TaskBridgeService(
|
||||
ITaskService taskService,
|
||||
IAgentService agentService,
|
||||
IActivityRepository activityRepo,
|
||||
INotificationService notificationService,
|
||||
ILiveUpdateService liveUpdateService) : ITaskBridgeService
|
||||
@@ -37,12 +38,11 @@ public sealed class TaskBridgeService(
|
||||
return Error<DashboardTaskDto>(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);
|
||||
title.Trim(), detail?.Trim(), normalizedSource, priority, assignedTo, parentTaskId: null, ct);
|
||||
|
||||
var dto = MapToDto(task);
|
||||
var dto = await taskService.GetDashboardTaskByIdAsync(task.Id, ct) ?? MapToDto(task);
|
||||
return Success(dto);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ public sealed class TaskBridgeService(
|
||||
string? priority = "Normal",
|
||||
string? assignedTo = null,
|
||||
string? expectedFrom = null,
|
||||
bool startsInProgress = false,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
@@ -66,19 +67,23 @@ public sealed class TaskBridgeService(
|
||||
if (parent is null)
|
||||
return Error<DashboardTaskDto>(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);
|
||||
priority, assignedTo, expectedFrom, parentTaskId, startsInProgress, null, 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 parentTransition = await taskService.StartCoordinationAsync(parentTaskId, ct);
|
||||
if (parentTransition.Outcome != TaskOperationOutcome.Success)
|
||||
{
|
||||
return Error<DashboardTaskDto>(
|
||||
TaskBridgeOutcome.InvalidState,
|
||||
$"Parent task {parentTaskId} could not be moved to In progress for coordination.");
|
||||
}
|
||||
}
|
||||
|
||||
var dto = MapToDto(task);
|
||||
var dto = await taskService.GetDashboardTaskByIdAsync(task.Id, ct) ?? MapToDto(task);
|
||||
return Success(dto);
|
||||
}
|
||||
|
||||
@@ -107,7 +112,7 @@ public sealed class TaskBridgeService(
|
||||
if (result.Outcome != TaskOperationOutcome.Success)
|
||||
return Error<DashboardTaskDto>(TaskBridgeOutcome.InvalidState, "Status update rejected.");
|
||||
|
||||
var dto = MapToDto(result.Task!);
|
||||
var dto = await taskService.GetDashboardTaskByIdAsync(result.Task!.Id, ct) ?? MapToDto(result.Task);
|
||||
return Success(dto);
|
||||
}
|
||||
|
||||
@@ -157,7 +162,10 @@ public sealed class TaskBridgeService(
|
||||
if (task is null)
|
||||
return Error<DashboardTaskDto>(TaskBridgeOutcome.NotFound, $"Task {taskId} not found.");
|
||||
|
||||
var normalizedTarget = targetAgent.Trim().ToLowerInvariant();
|
||||
var normalizedTarget = await NormalizeActorAsync(targetAgent, ct);
|
||||
if (normalizedTarget is null)
|
||||
return Error<DashboardTaskDto>(TaskBridgeOutcome.ValidationError, $"Unknown target agent '{targetAgent}'.");
|
||||
|
||||
var handoffNote = string.IsNullOrWhiteSpace(note)
|
||||
? $"Handoff → {normalizedTarget}"
|
||||
: $"Handoff → {normalizedTarget}: {note.Trim()}";
|
||||
@@ -186,7 +194,7 @@ public sealed class TaskBridgeService(
|
||||
task.Id,
|
||||
ct);
|
||||
|
||||
var dto = MapToDto(task);
|
||||
var dto = await taskService.GetDashboardTaskByIdAsync(task.Id, ct) ?? MapToDto(task);
|
||||
return Success(dto);
|
||||
}
|
||||
|
||||
@@ -207,8 +215,11 @@ public sealed class TaskBridgeService(
|
||||
public async Task<IReadOnlyList<DashboardTaskDto>> GetChildTasksAsync(
|
||||
Guid parentTaskId, CancellationToken ct = default)
|
||||
{
|
||||
var children = await taskService.GetChildTasksAsync(parentTaskId, ct);
|
||||
return children.Select(MapToDto).ToList();
|
||||
var board = await taskService.GetBoardAsync(ct);
|
||||
return FlattenBoard(board)
|
||||
.Where(task => task.ParentTaskId == parentTaskId)
|
||||
.OrderByDescending(task => task.UpdatedAt)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<ActivityEvent>> GetTaskActivityAsync(
|
||||
@@ -233,14 +244,19 @@ public sealed class TaskBridgeService(
|
||||
private static string NormalizeSource(string? source) =>
|
||||
string.IsNullOrWhiteSpace(source) ? "iris" : source.Trim().ToLowerInvariant();
|
||||
|
||||
private static string? NormalizeAssignedTo(string? assignedTo)
|
||||
private async Task<string?> NormalizeActorAsync(string? actorId, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(assignedTo)) return null;
|
||||
var valid = new HashSet<string> { "bao", "iris", "programmer", "reviewer", "architekt", "researcher", "executor" };
|
||||
var lower = assignedTo.Trim().ToLowerInvariant();
|
||||
return valid.Contains(lower) ? lower : null;
|
||||
var allowedActors = AgentIdentityCatalog.BuildAllowedActorIds(await agentService.GetAllowedAgentIdsAsync(ct));
|
||||
return AgentIdentityCatalog.NormalizeActorId(actorId, allowedActors);
|
||||
}
|
||||
|
||||
private static IEnumerable<DashboardTaskDto> FlattenBoard(BoardResponse board)
|
||||
=> board.Offen
|
||||
.Concat(board.InProgress)
|
||||
.Concat(board.Review)
|
||||
.Concat(board.Blocked)
|
||||
.Concat(board.Done);
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user