Saltar a contenido

Calmia Nexus - Guía Técnica para Desarrolladores

Versión: 1.0 Última actualización: 2026-02-01 Audiencia: Desarrolladores, Tech Leads, Arquitectos


Índice

  1. Arquitectura General
  2. Estructura de Proyectos
  3. Capas de la Aplicación
  4. Modelos de Datos (Entidades)
  5. DTOs (Data Transfer Objects)
  6. Servicios de Dominio
  7. API REST Endpoints
  8. SignalR Hubs (Real-time)
  9. Entity Framework y DbContext
  10. Patrones y Convenciones
  11. Flujos de Datos Principales
  12. Guía de Integración
  13. Testing
  14. Troubleshooting

1. Arquitectura General

1.1 Diagrama de Arquitectura

graph TB
    subgraph "Frontend"
        ADMIN[Orchestrator.Admin<br/>Blazor UI]
    end

    subgraph "Backend"
        API[Orchestrator.Api<br/>REST + SignalR]
        WORKERS[Orchestrator.Workers<br/>Background Jobs]
        MCP[Orchestrator.Mcp<br/>MCP Protocol]
        MCP_REMOTE[Orchestrator.Mcp.Remote<br/>Remote MCP Server]
    end

    subgraph "Shared Libraries"
        SHARED_ADMIN[Shared.Admin<br/>Entities, Services, DTOs]
        SHARED_EVENTS[Shared.Events<br/>Domain Events]
        SHARED_LOGGING[Shared.Logging<br/>Structured Logging]
    end

    subgraph "External Services"
        CLAUDE[Claude API<br/>Anthropic]
        CLICKUP[ClickUp API]
        SENTRY[Sentry]
        WHATSAPP[WhatsApp<br/>UltraMsg]
    end

    subgraph "Infrastructure"
        PG[(PostgreSQL)]
        RABBIT[RabbitMQ]
    end

    ADMIN -->|HTTP/SignalR| API
    API --> SHARED_ADMIN
    WORKERS --> SHARED_ADMIN
    MCP --> SHARED_ADMIN
    SHARED_ADMIN --> PG

    API -->|SSE/HTTP| CLAUDE
    API -->|REST| CLICKUP
    API -->|SDK| SENTRY
    WORKERS -->|REST| WHATSAPP

    API <-->|Pub/Sub| RABBIT
    WORKERS <-->|Pub/Sub| RABBIT

1.2 Stack Tecnológico

Capa Tecnología Versión
Runtime .NET 8.0/9.0
Web Framework ASP.NET Core 8.0
UI Framework Blazor Server 8.0
UI Components MudBlazor 7.x
ORM Entity Framework Core 8.0
Database PostgreSQL 16.x
Real-time SignalR 8.0
Messaging RabbitMQ 3.x
Logging Serilog 3.x
Error Tracking Sentry 4.x

1.3 Principios de Diseño

  1. Clean Architecture - Separación de responsabilidades por capas
  2. CQRS Lite - Separación de queries y commands en servicios
  3. Domain-Driven Design - Entidades ricas con lógica de negocio
  4. Multi-Tenancy - Aislamiento por OrganizationId
  5. Event-Driven - Comunicación asíncrona entre componentes

2. Estructura de Proyectos

platform/
├── Orchestrator/
│   └── src/
│       ├── Orchestrator.Api/           # API REST + SignalR
│       │   ├── Controllers/            # Endpoints HTTP
│       │   ├── Hubs/                   # SignalR Hubs
│       │   └── Program.cs              # Entry point + DI
│       │
│       ├── Orchestrator.Admin/         # Blazor UI
│       │   ├── Components/
│       │   │   ├── Layout/             # MainLayout, EmptyLayout
│       │   │   ├── Pages/              # Páginas Blazor
│       │   │   └── Shared/             # Componentes reutilizables
│       │   ├── Services/               # Servicios de UI
│       │   └── Theme/                  # Temas MudBlazor
│       │
│       ├── Orchestrator.Workers/       # Background Jobs
│       │   └── Workers/                # Hosted Services
│       │
│       ├── Orchestrator.Mcp/           # MCP Protocol
│       └── Orchestrator.Mcp.Remote/    # Remote MCP Server
├── Shared/
│   ├── Shared.Admin/
│   │   ├── Data/
│   │   │   ├── NexusDbContext.cs       # DbContext principal
│   │   │   ├── NexusDbContextFactory.cs
│   │   │   └── Migrations/             # EF Migrations
│   │   ├── Dtos/                       # Data Transfer Objects
│   │   ├── Entities/                   # Modelos de dominio
│   │   └── Services/                   # Servicios de dominio
│   │
│   ├── Shared.Events/                  # Eventos de dominio
│   └── Shared.Logging/                 # Logging estructurado
├── Messaging/
│   └── WhatsappService/                # Servicio de WhatsApp
└── Tools/
    ├── NexusRemoteAgent/               # Agente remoto CLI
    └── TunnelClient/                   # Cliente de túnel

2.1 Dependencias entre Proyectos

graph LR
    API[Orchestrator.Api] --> SA[Shared.Admin]
    ADMIN[Orchestrator.Admin] --> SA
    WORKERS[Orchestrator.Workers] --> SA
    MCP[Orchestrator.Mcp] --> SA

    SA --> SE[Shared.Events]
    SA --> SL[Shared.Logging]

    WHATSAPP[WhatsappService] --> SE

3. Capas de la Aplicación

3.1 Capa de Presentación (Orchestrator.Admin)

Components/
├── Layout/
│   ├── MainLayout.razor              # Layout principal con sidebar
│   └── EmptyLayout.razor             # Layout minimalista
├── Pages/
│   ├── Workspace/
│   │   ├── Workspace.razor           # Centro de trabajo principal
│   │   ├── ConversationArea.razor    # Área de chat
│   │   ├── MessageInput.razor        # Input de mensajes
│   │   ├── ContextPanel.razor        # Panel de contexto
│   │   ├── BacklogMiniPanel.razor    # Panel de backlog
│   │   └── MemoriesPanel.razor       # Panel de memorias
│   │
│   ├── Agents/
│   │   ├── AgentList.razor           # Lista de agentes
│   │   └── AgentDetail.razor         # Detalle de agente
│   │
│   ├── Projects/
│   │   ├── ProjectList.razor         # Lista de proyectos
│   │   └── ProjectDetail.razor       # Detalle de proyecto
│   │
│   └── Dashboard/
│       └── Dashboard.razor           # Dashboard principal
└── Shared/
    ├── OrganizationProjectSelector.razor
    ├── ThemeSwitcher.razor
    └── ConfirmDialog.razor

3.2 Capa de API (Orchestrator.Api)

Controllers/
├── BacklogController.cs              # /api/backlog
├── DevSessionsController.cs          # /api/dev-sessions
├── CopilotController.cs              # /api/copilot
├── AIController.cs                   # /api/ai
├── ProjectsController.cs             # /api/projects
├── AgentsController.cs               # /api/agents
├── ExecutionsController.cs           # /api/executions
└── ... (30+ controllers)

Hubs/
├── AdminHub.cs                       # Dashboard + Workspace
├── RemoteAgentHub.cs                 # Agentes remotos
└── TunnelHub.cs                      # Túneles SSH

3.3 Capa de Servicios (Shared.Admin/Services)

Services/
├── Core/
│   ├── DevSessionService.cs          # Sesiones interactivas
│   ├── CopilotService.cs             # Copiloto autónomo
│   ├── BacklogService.cs             # Gestión de backlog
│   └── ExecutionService.cs           # Ciclo de ejecuciones
├── AI/
│   ├── AIProviderService.cs          # Proveedores de IA
│   ├── AISuggestionService.cs        # Sugerencias IA
│   ├── CopilotPlanGenerator.cs       # Generación de planes
│   └── BacklogAIProcessorService.cs  # Análisis de backlog
├── Integration/
│   ├── ClickUpSyncService.cs         # Sincronización ClickUp
│   ├── SentryService.cs              # Integración Sentry
│   └── RemoteAgentService.cs         # Agentes remotos
└── Support/
    ├── MemoryService.cs              # Memoria de usuario
    ├── HistoryManagerService.cs      # Optimización historial
    └── WorkspaceEnrichmentService.cs # Enriquecimiento

3.4 Capa de Datos (Shared.Admin/Data)

Data/
├── NexusDbContext.cs                 # 64+ DbSets
├── NexusDbContextFactory.cs          # Design-time factory
├── NexusDbSeeder.cs                  # Datos iniciales
└── Migrations/                       # 19+ migrations

4. Modelos de Datos (Entidades)

4.1 Entidades Principales

Organization

public class Organization
{
    public Guid Id { get; set; }
    public required string Slug { get; set; }           // Único
    public required string Name { get; set; }
    public string? Description { get; set; }
    public string? LogoUrl { get; set; }
    public string? ThemeId { get; set; }
    public string? PrimaryColor { get; set; }
    public bool IsActive { get; set; } = true;
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }

    // Navegación
    public List<Project> Projects { get; set; } = [];
    public List<CommunicationChannel> CommunicationChannels { get; set; } = [];
}

Project

public class Project
{
    public Guid Id { get; set; }
    public required string Slug { get; set; }           // Único
    public required string Name { get; set; }
    public string? Description { get; set; }
    public string? RepoPath { get; set; }
    public string? Instructions { get; set; }           // Inyectables en sesiones

    // Configuración Sentry
    public string? SentryConfigJson { get; set; }       // JSONB

    // Configuración de documentos
    public bool EnableDocumentContextualization { get; set; }
    public int MaxDocumentsPerRequest { get; set; } = 3;
    public double MinDocumentRelevance { get; set; } = 0.3;

    // Navegación
    public List<ProjectSecret> Secrets { get; set; } = [];
    public List<ScheduledTask> Tasks { get; set; } = [];
    public List<Execution> Executions { get; set; } = [];
    public List<BacklogItem> BacklogItems { get; set; } = [];
}

DevSession

public class DevSession
{
    public Guid Id { get; set; }
    public required string Name { get; set; }
    public string? Description { get; set; }

    // Referencias
    public Guid? ProjectId { get; set; }
    public string? AgentId { get; set; }
    public string? PluginId { get; set; }
    public Guid? RemoteAgentId { get; set; }
    public Guid UserId { get; set; }
    public Guid OrganizationId { get; set; }
    public Guid? BacklogItemId { get; set; }            // Tarea origen

    // Estado
    public string Status { get; set; } = "active";      // active, completed, archived
    public int MessageCount { get; set; }
    public int TokensUsed { get; set; }

    // Instrucciones
    public string? SessionInstructions { get; set; }
    public bool InjectSessionInstructions { get; set; } = true;

    // Optimización de historial
    public int MaxHistoryMessages { get; set; } = 20;
    public int MaxHistoryTokens { get; set; } = 50000;
    public bool AutoSummarizeHistory { get; set; } = true;
    public string? HistorySummary { get; set; }
    public DateTime? HistorySummaryGeneratedAt { get; set; }
    public int? HistorySummaryMessageCount { get; set; }

    // Work tracking
    public DateTime? WorkStartedAt { get; set; }
    public DateTime? WorkEndedAt { get; set; }

    // Navegación
    public List<DevSessionMessage> Messages { get; set; } = [];
}

CopilotExecution

public class CopilotExecution
{
    public Guid Id { get; set; }
    public Guid DevSessionId { get; set; }
    public required string Objective { get; set; }

    // Estado (máquina de estados)
    public CopilotState State { get; set; } = CopilotState.Idle;
    public CopilotResult Result { get; set; } = CopilotResult.Pending;

    // Progreso
    public int TotalSteps { get; set; }
    public int CurrentStepIndex { get; set; }
    public int CompletedSteps { get; set; }
    public int FailedSteps { get; set; }

    // Configuración
    public bool AutoApprove { get; set; } = false;
    public bool AutoContinue { get; set; } = true;
    public bool PauseOnWarning { get; set; } = false;
    public int MaxIterations { get; set; } = 10;
    public int MaxRetries { get; set; } = 3;
    public int TimeoutMinutes { get; set; } = 30;
    public int StepTimeoutSeconds { get; set; } = 300;

    // Métricas
    public int TotalIterations { get; set; }
    public int TotalTokensUsed { get; set; }
    public int TotalMessagesGenerated { get; set; }
    public long? TotalDurationMs { get; set; }

    // Batch processing
    public Guid? BatchId { get; set; }

    // Datos adicionales (JSONB)
    public string? ArtifactsJson { get; set; }
    public string? WarningsJson { get; set; }

    // Navegación
    public List<CopilotStep> Steps { get; set; } = [];
    public DevSession? DevSession { get; set; }
}

public enum CopilotState
{
    Idle,
    Planning,
    AwaitingApproval,
    Executing,
    Paused,
    Completed,
    Failed,
    Cancelled
}

public enum CopilotResult
{
    Pending,
    Success,
    Warning,
    Failed
}

4.2 Diagrama de Relaciones

erDiagram
    Organization ||--o{ Project : has
    Organization ||--o{ User : "members via"
    Organization ||--o{ DevSession : contains

    Project ||--o{ ScheduledTask : has
    Project ||--o{ Execution : has
    Project ||--o{ BacklogItem : has
    Project ||--o{ ProjectSecret : has
    Project |o--o| ClickUpConfig : "syncs with"

    User ||--o{ DevSession : creates
    User ||--o{ UserMemory : has
    User ||--o{ WellnessRecord : tracks

    DevSession ||--o{ DevSessionMessage : contains
    DevSession ||--o{ CopilotExecution : "can have"
    DevSession |o--|| BacklogItem : "originates from"

    CopilotExecution ||--o{ CopilotStep : has
    CopilotExecution |o--|| CopilotBatchExecution : "part of"

    ScheduledTask ||--o{ Execution : generates
    Execution ||--o{ BacklogItem : creates

    Agent ||--o{ AgentSkill : has
    Agent ||--o{ AgentTrigger : has

    Plugin ||--o{ PluginAgent : "includes"
    Plugin ||--o{ PluginSkill : "includes"
    Plugin ||--o{ PluginCommand : "includes"
    Plugin ||--o{ PluginProcedure : "includes"

5. DTOs (Data Transfer Objects)

5.1 Convención de Nombres

Sufijo Propósito Ejemplo
Dto Transferencia general BacklogItemDto
ListDto Listados (menos campos) BacklogItemListDto
DetailDto Detalle (todos los campos) BacklogItemDetailDto
CreateRequest Creación CreateBacklogItemRequest
UpdateRequest Actualización UpdateBacklogItemRequest
Response Respuesta de API BacklogListResponse

5.2 DTOs Principales

BacklogDtos.cs

// Para listados rápidos (mini panel)
public record BacklogQuickItemDto(
    Guid Id,
    string Title,
    string Status,
    string? Priority,
    string? ItemType,
    DateTime CreatedAt
);

// Para listados completos
public record BacklogItemListDto(
    Guid Id,
    string Title,
    string Status,
    string? Priority,
    string? ItemType,
    string? AICategory,
    string? EstimatedEffort,
    DateTime CreatedAt,
    DateTime? UpdatedAt
);

// Para detalle
public record BacklogItemDetailDto(
    Guid Id,
    string Title,
    string? Description,
    string Status,
    string? Priority,
    string? ItemType,
    string? AICategory,
    string? AISummary,
    string? EstimatedEffort,
    string? Source,
    string? SentryIssueId,
    string? SentryPermalink,
    string? ClickUpTaskId,
    string? ClickUpUrl,
    Guid? ProjectId,
    Guid? ExecutionId,
    Guid? ConvertedToTaskId,
    DateTime CreatedAt,
    DateTime? UpdatedAt,
    DateTime? ResolvedAt
);

// Para crear
public record CreateBacklogItemRequest(
    string Title,
    string? Description,
    string? ItemType,
    string? Priority,
    Guid ProjectId
);

// Para convertir a tarea
public record ConvertToTaskRequest(
    Guid BacklogItemId,
    string? AgentId,
    string? ProcedureId,
    bool ExecuteImmediately = false
);

DevSessionDtos.cs

public record DevSessionListDto(
    Guid Id,
    string Name,
    string Status,
    int MessageCount,
    int TokensUsed,
    string? AgentId,
    Guid? ProjectId,
    DateTime CreatedAt,
    DateTime? UpdatedAt
);

public record DevSessionDetailDto(
    Guid Id,
    string Name,
    string? Description,
    string Status,
    int MessageCount,
    int TokensUsed,
    string? AgentId,
    string? PluginId,
    Guid? ProjectId,
    Guid? RemoteAgentId,
    Guid? BacklogItemId,
    string? SessionInstructions,
    bool InjectSessionInstructions,
    int MaxHistoryMessages,
    bool AutoSummarizeHistory,
    DateTime CreatedAt,
    DateTime? UpdatedAt,
    List<DevSessionMessageDto> Messages
);

public record CreateDevSessionRequest(
    string Name,
    string? Description,
    Guid? ProjectId,
    string? AgentId,
    string? PluginId,
    Guid? BacklogItemId
);

public record DevSessionSendMessageRequest(
    string Content,
    string? Model = null,
    bool Stream = true
);

CopilotDtos.cs

public record CopilotExecutionDto(
    Guid Id,
    Guid DevSessionId,
    string Objective,
    string State,
    string Result,
    int TotalSteps,
    int CurrentStepIndex,
    int CompletedSteps,
    int FailedSteps,
    bool AutoApprove,
    bool AutoContinue,
    int TotalTokensUsed,
    long? TotalDurationMs,
    DateTime CreatedAt,
    DateTime? CompletedAt,
    List<CopilotStepDto> Steps
);

public record CopilotStepDto(
    Guid Id,
    int Order,
    string Title,
    string? Description,
    string Status,
    string? Category,
    int? ResponseTokens,
    int RetryCount,
    long? DurationMs,
    DateTime CreatedAt,
    DateTime? CompletedAt
);

public record StartCopilotRequest(
    string Objective,
    bool AutoApprove = false,
    bool AutoContinue = true,
    int MaxIterations = 10,
    int TimeoutMinutes = 30
);

public record ModifyPlanRequest(
    string Instructions
);

6. Servicios de Dominio

6.1 Servicios Principales

DevSessionService

public interface IDevSessionService
{
    Task<DevSession> CreateSessionAsync(CreateDevSessionRequest request, Guid userId, Guid organizationId);
    Task<DevSession?> GetSessionAsync(Guid sessionId);
    Task<List<DevSessionListDto>> GetSessionsAsync(Guid userId, Guid? projectId = null);
    Task<DevSessionMessage> SendMessageAsync(Guid sessionId, string content, string? model = null);
    Task SendMessageStreamAsync(Guid sessionId, string content, string? model, Func<string, Task> onChunk, CancellationToken ct = default);
    Task<DevSession> UpdateSessionAsync(Guid sessionId, UpdateDevSessionRequest request);
    Task DeleteSessionAsync(Guid sessionId);
}

// Implementación con optimización de historial
public class DevSessionService : IDevSessionService
{
    private readonly NexusDbContext _db;
    private readonly IAIProviderService _aiProvider;
    private readonly IHistoryManagerService? _historyManager;
    private readonly ConcurrentDictionary<Guid, SemaphoreSlim> _sessionLocks = new();

    public async Task SendMessageStreamAsync(
        Guid sessionId,
        string content,
        string? model,
        Func<string, Task> onChunk,
        CancellationToken ct = default)
    {
        // Obtener lock por sesión
        var sessionLock = _sessionLocks.GetOrAdd(sessionId, _ => new SemaphoreSlim(1, 1));
        await sessionLock.WaitAsync(ct);

        try
        {
            var session = await _db.DevSessions
                .Include(s => s.Messages.OrderBy(m => m.CreatedAt))
                .FirstOrDefaultAsync(s => s.Id == sessionId, ct);

            if (session == null) throw new NotFoundException("Session not found");

            // Optimizar historial si está habilitado
            List<DevSessionMessage> historyMessages;
            string? historySummary = null;

            if (_historyManager != null && session.AutoSummarizeHistory)
            {
                var optimizedHistory = await _historyManager.GetOptimizedHistoryAsync(sessionId);
                historyMessages = optimizedHistory.RecentMessages;
                historySummary = optimizedHistory.Summary;
            }
            else
            {
                historyMessages = session.Messages.TakeLast(session.MaxHistoryMessages).ToList();
            }

            // Construir contexto
            var messages = BuildConversationMessages(historyMessages, historySummary, session);
            messages.Add(new { role = "user", content });

            // Stream response desde Claude
            await _aiProvider.StreamCompletionAsync(
                model ?? "claude-sonnet-4-20250514",
                messages,
                async chunk => await onChunk(chunk),
                ct
            );

            // Guardar mensajes
            await SaveMessagesAsync(session, content, fullResponse, model);
        }
        finally
        {
            sessionLock.Release();
        }
    }
}

CopilotService

public interface ICopilotService
{
    Task<CopilotExecution> StartAsync(Guid sessionId, StartCopilotRequest request);
    Task<CopilotExecution> PauseAsync(Guid executionId);
    Task<CopilotExecution> ResumeAsync(Guid executionId);
    Task<CopilotExecution> CancelAsync(Guid executionId);
    Task<CopilotExecution> ApprovePlanAsync(Guid executionId);
    Task<CopilotExecution> ModifyPlanAsync(Guid executionId, ModifyPlanRequest request);
    Task<CopilotExecution> SkipStepAsync(Guid executionId, int stepIndex);
}

// Máquina de estados del Copiloto
public class CopilotService : ICopilotService
{
    // Estados válidos
    // Idle -> Planning -> AwaitingApproval -> Executing -> Completed/Failed
    //                           |                 |
    //                           v                 v
    //                        Cancelled         Paused -> Executing

    private readonly SemaphoreSlim _stateLock = new(1, 1);

    public async Task<CopilotExecution> StartAsync(Guid sessionId, StartCopilotRequest request)
    {
        await _stateLock.WaitAsync();
        try
        {
            // 1. Crear ejecución
            var execution = new CopilotExecution
            {
                DevSessionId = sessionId,
                Objective = request.Objective,
                State = CopilotState.Planning,
                AutoApprove = request.AutoApprove,
                AutoContinue = request.AutoContinue,
                MaxIterations = request.MaxIterations,
                TimeoutMinutes = request.TimeoutMinutes
            };

            _db.CopilotExecutions.Add(execution);
            await _db.SaveChangesAsync();

            // 2. Generar plan
            var plan = await _planGenerator.GeneratePlanAsync(
                execution.Objective,
                sessionId
            );

            // 3. Crear steps
            foreach (var (step, index) in plan.Steps.Select((s, i) => (s, i)))
            {
                execution.Steps.Add(new CopilotStep
                {
                    ExecutionId = execution.Id,
                    Order = index,
                    Title = step.Title,
                    Description = step.Description,
                    Status = CopilotStepStatus.Pending
                });
            }

            execution.TotalSteps = execution.Steps.Count;

            // 4. Transición de estado
            if (request.AutoApprove)
            {
                execution.State = CopilotState.Executing;
                _ = Task.Run(() => ExecuteStepsAsync(execution.Id));
            }
            else
            {
                execution.State = CopilotState.AwaitingApproval;
            }

            await _db.SaveChangesAsync();

            // 5. Notificar
            await _adminHub.Clients.Group($"session-{sessionId}")
                .SendAsync("CopilotStateChanged", new CopilotStateMessage(execution));

            return execution;
        }
        finally
        {
            _stateLock.Release();
        }
    }
}

BacklogService

public interface IBacklogService
{
    Task<BacklogItem> CreateAsync(CreateBacklogItemRequest request, Guid userId);
    Task<BacklogItem?> GetAsync(Guid id);
    Task<BacklogListResponse> GetListAsync(BacklogListRequest request);
    Task<List<BacklogQuickItemDto>> GetQuickListAsync(Guid projectId, int limit = 10);
    Task<BacklogItem> UpdateAsync(Guid id, UpdateBacklogItemRequest request);
    Task DeleteAsync(Guid id);
    Task<ScheduledTask> ConvertToTaskAsync(ConvertToTaskRequest request);
    Task SyncWithClickUpAsync(Guid projectId);
}

public class BacklogService : IBacklogService
{
    public async Task<ScheduledTask> ConvertToTaskAsync(ConvertToTaskRequest request)
    {
        var backlogItem = await _db.BacklogItems
            .FirstOrDefaultAsync(b => b.Id == request.BacklogItemId)
            ?? throw new NotFoundException("Backlog item not found");

        // Crear tarea programada
        var task = new ScheduledTask
        {
            Slug = $"backlog-{backlogItem.Id:N}".Substring(0, 20),
            Name = backlogItem.Title,
            TaskType = "copilot",
            AgentId = request.AgentId,
            ProcedureId = request.ProcedureId,
            ProjectId = backlogItem.ProjectId,
            SourceBacklogItemId = backlogItem.Id,
            Priority = backlogItem.Priority ?? "medium",
            IsOneTimeExecution = true
        };

        _db.ScheduledTasks.Add(task);
        backlogItem.ConvertedToTaskId = task.Id;
        backlogItem.Status = "in_progress";

        await _db.SaveChangesAsync();

        // Ejecutar inmediatamente si se solicita
        if (request.ExecuteImmediately)
        {
            await _executionService.TriggerAsync(task.Id);
        }

        return task;
    }
}

6.2 Servicios de Integración

ClickUpSyncService

public interface IClickUpSyncService
{
    Task<ClickUpSyncResult> SyncAsync(Guid projectId);
    Task<string?> CreateInClickUpAsync(BacklogItem item);
    Task UpdateInClickUpAsync(BacklogItem item);
    Task<List<ClickUpTask>> PullFromClickUpAsync(Guid projectId);
}

public class ClickUpSyncService : IClickUpSyncService
{
    private readonly HttpClient _httpClient;
    private readonly NexusDbContext _db;

    public async Task<string?> CreateInClickUpAsync(BacklogItem item)
    {
        var config = await GetConfigAsync(item.ProjectId);
        if (config == null || !config.SyncEnabled) return null;

        var payload = new
        {
            name = item.Title,
            description = item.Description,
            status = MapStatusToClickUp(item.Status, config),
            priority = MapPriorityToClickUp(item.Priority)
        };

        var response = await _httpClient.PostAsJsonAsync(
            $"https://api.clickup.com/api/v2/list/{config.ClickUpListId}/task",
            payload
        );

        // Log sync operation
        await LogSyncAsync(item.Id, item.ProjectId, "create", "push", response);

        if (response.IsSuccessStatusCode)
        {
            var result = await response.Content.ReadFromJsonAsync<ClickUpTaskResponse>();
            return result?.Id;
        }

        return null;
    }
}

6.3 Diagrama de Dependencias

graph TB
    subgraph "API Layer"
        BC[BacklogController]
        DC[DevSessionsController]
        CC[CopilotController]
    end

    subgraph "Service Layer"
        BS[BacklogService]
        DS[DevSessionService]
        CS[CopilotService]
        HMS[HistoryManagerService]
        AIS[AISuggestionService]
        CUS[ClickUpSyncService]
    end

    subgraph "Data Layer"
        DB[(NexusDbContext)]
    end

    subgraph "External"
        CLAUDE[Claude API]
        CLICKUP[ClickUp API]
    end

    BC --> BS
    DC --> DS
    CC --> CS

    BS --> DB
    BS --> CUS
    BS --> AIS

    DS --> DB
    DS --> HMS
    DS --> CLAUDE

    CS --> DB
    CS --> DS

    CUS --> CLICKUP

7. API REST Endpoints

7.1 Endpoints Principales

Backlog API

# Listar items (con filtros y paginación)
GET /api/backlog?projectId={guid}&status={status}&page={int}&pageSize={int}

# Obtener item rápido (para mini panel)
GET /api/backlog/quick?projectId={guid}&limit={int}

# Obtener detalle
GET /api/backlog/{id}

# Crear item
POST /api/backlog
Content-Type: application/json
{
    "title": "Implementar feature X",
    "description": "Descripción detallada...",
    "itemType": "feature",
    "priority": "high",
    "projectId": "guid"
}

# Actualizar item
PUT /api/backlog/{id}
Content-Type: application/json
{
    "title": "Título actualizado",
    "status": "in_progress"
}

# Convertir a tarea
POST /api/backlog/{id}/convert-to-task
Content-Type: application/json
{
    "agentId": "code-assistant",
    "procedureId": "implement-feature",
    "executeImmediately": true
}

# Eliminar item
DELETE /api/backlog/{id}

DevSessions API

# Listar sesiones
GET /api/dev-sessions?projectId={guid}&status={status}

# Obtener sesión con mensajes
GET /api/dev-sessions/{id}

# Crear sesión
POST /api/dev-sessions
Content-Type: application/json
{
    "name": "Feature X Development",
    "projectId": "guid",
    "agentId": "code-assistant",
    "backlogItemId": "guid"
}

# Enviar mensaje (streaming)
POST /api/dev-sessions/{id}/messages
Content-Type: application/json
{
    "content": "Implementa la función de login",
    "model": "claude-sonnet-4-20250514",
    "stream": true
}

# Respuesta streaming (SSE)
HTTP/1.1 200 OK
Content-Type: text/event-stream

data: {"chunk": "Voy a implementar"}
data: {"chunk": " la función de login"}
data: {"chunk": " usando JWT..."}
data: [DONE]

# Actualizar sesión
PUT /api/dev-sessions/{id}
Content-Type: application/json
{
    "name": "Nuevo nombre",
    "sessionInstructions": "Instrucciones específicas..."
}

Copilot API

# Iniciar copiloto
POST /api/copilot/start
Content-Type: application/json
{
    "sessionId": "guid",
    "objective": "Implementar sistema de autenticación completo",
    "autoApprove": false,
    "maxIterations": 10,
    "timeoutMinutes": 30
}

# Obtener estado
GET /api/copilot/{executionId}

# Aprobar plan
POST /api/copilot/{executionId}/approve

# Modificar plan
POST /api/copilot/{executionId}/modify
Content-Type: application/json
{
    "instructions": "Añade un paso para validación de email"
}

# Pausar ejecución
POST /api/copilot/{executionId}/pause

# Reanudar ejecución
POST /api/copilot/{executionId}/resume

# Cancelar ejecución
POST /api/copilot/{executionId}/cancel

# Saltar paso
POST /api/copilot/{executionId}/skip/{stepIndex}

7.2 Tabla de Endpoints

Controller Ruta Base Métodos Descripción
BacklogController /api/backlog GET, POST, PUT, DELETE Gestión de backlog
DevSessionsController /api/dev-sessions GET, POST, PUT, DELETE, POST/messages Sesiones de desarrollo
CopilotController /api/copilot POST, GET, POST/* Copiloto autónomo
CopilotBatchController /api/copilot-batch POST, GET, POST/* Batch processing
AIController /api/ai POST Servicios de IA
ProjectsController /api/projects CRUD Gestión de proyectos
AgentsController /api/agents CRUD Gestión de agentes
ExecutionsController /api/executions GET, POST Historial de ejecuciones
TasksController /api/tasks CRUD Tareas programadas
RemoteAgentsController /api/remote-agents CRUD Agentes remotos
MemoriesController /api/memories CRUD Memoria de usuario
ContextSnippetsController /api/context-snippets CRUD Fragmentos de contexto

8. SignalR Hubs (Real-time)

8.1 AdminHub

// Hub para comunicación con dashboard y workspace
[Authorize]
public class AdminHub : Hub
{
    // Grupos disponibles
    // - dashboard: Suscriptores del dashboard
    // - execution-{id}: Suscriptores a ejecución específica
    // - session-{id}: Suscriptores a sesión específica
    // - user-{id}: Notificaciones por usuario

    // Cliente -> Servidor
    public async Task SubscribeToDashboard()
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, "dashboard");
    }

    public async Task SubscribeToSession(string sessionId)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, $"session-{sessionId}");
    }

    public async Task UnsubscribeFromSession(string sessionId)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"session-{sessionId}");
    }

    // Servidor -> Cliente (llamados desde servicios)
    // ExecutionUpdated, CopilotStateChanged, NotificationReceived, etc.
}

// Mensajes tipados
public record ExecutionUpdateMessage(
    Guid ExecutionId,
    string Status,
    string? Output,
    DateTime Timestamp
);

public record CopilotStateMessage(
    Guid ExecutionId,
    string State,
    int CurrentStep,
    int TotalSteps,
    string? CurrentStepTitle
);

8.2 RemoteAgentHub

// Hub para comunicación con agentes remotos (Claude CLI)
public class RemoteAgentHub : Hub
{
    private readonly ConcurrentDictionary<string, Guid> _connectionToAgent = new();
    private readonly ConcurrentDictionary<Guid, string> _agentToConnection = new();
    private readonly ConcurrentDictionary<Guid, SemaphoreSlim> _commandLocks = new();

    // Agente -> Servidor
    public async Task RegisterAgent(string apiKey, string systemInfo, string agentVersion)
    {
        var agent = await _db.RemoteAgents
            .FirstOrDefaultAsync(a => a.ApiKeyHash == HashApiKey(apiKey));

        if (agent == null)
        {
            Context.Abort();
            return;
        }

        agent.ConnectionId = Context.ConnectionId;
        agent.Status = "Online";
        agent.SystemInfo = systemInfo;
        agent.AgentVersion = agentVersion;

        _connectionToAgent[Context.ConnectionId] = agent.Id;
        _agentToConnection[agent.Id] = Context.ConnectionId;

        await _db.SaveChangesAsync();
    }

    public async Task SendChunks(Guid commandId, int sequence, List<ChunkData> chunks)
    {
        // Procesar chunks de respuesta
    }

    public async Task CommandCompleted(Guid commandId, RemoteCommandResult result)
    {
        var command = await _db.RemoteAgentCommands.FindAsync(commandId);
        if (command != null)
        {
            command.Status = "Completed";
            command.Result = result.Output;
            await _db.SaveChangesAsync();
        }
    }

    // Servidor -> Agente
    public async Task ExecuteCommandAsync(Guid agentId, Guid commandId, string prompt, string workingDirectory)
    {
        if (_agentToConnection.TryGetValue(agentId, out var connectionId))
        {
            await Clients.Client(connectionId).SendAsync(
                "ExecuteCommand",
                commandId,
                prompt,
                workingDirectory
            );
        }
    }
}

8.3 Uso desde Blazor

// En el componente Workspace.razor
@inject SignalRService SignalR

@code {
    private HubConnection? _hubConnection;

    protected override async Task OnInitializedAsync()
    {
        _hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/hubs/admin"))
            .WithAutomaticReconnect()
            .Build();

        // Suscribirse a eventos
        _hubConnection.On<CopilotStateMessage>("CopilotStateChanged", message =>
        {
            // Actualizar UI
            _copilotState = message.State;
            _currentStep = message.CurrentStep;
            StateHasChanged();
        });

        await _hubConnection.StartAsync();
        await _hubConnection.InvokeAsync("SubscribeToSession", SessionId.ToString());
    }

    public async ValueTask DisposeAsync()
    {
        if (_hubConnection != null)
        {
            await _hubConnection.InvokeAsync("UnsubscribeFromSession", SessionId.ToString());
            await _hubConnection.DisposeAsync();
        }
    }
}

9. Entity Framework y DbContext

9.1 Configuración del DbContext

public class NexusDbContext : DbContext
{
    // 64+ DbSets
    public DbSet<Organization> Organizations => Set<Organization>();
    public DbSet<Project> Projects => Set<Project>();
    public DbSet<User> Users => Set<User>();
    public DbSet<DevSession> DevSessions => Set<DevSession>();
    public DbSet<DevSessionMessage> DevSessionMessages => Set<DevSessionMessage>();
    public DbSet<CopilotExecution> CopilotExecutions => Set<CopilotExecution>();
    public DbSet<CopilotStep> CopilotSteps => Set<CopilotStep>();
    public DbSet<BacklogItem> BacklogItems => Set<BacklogItem>();
    public DbSet<ScheduledTask> ScheduledTasks => Set<ScheduledTask>();
    public DbSet<Execution> Executions => Set<Execution>();
    public DbSet<Agent> Agents => Set<Agent>();
    public DbSet<RemoteAgent> RemoteAgents => Set<RemoteAgent>();
    // ... más DbSets

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Índices únicos
        modelBuilder.Entity<Organization>()
            .HasIndex(o => o.Slug)
            .IsUnique();

        modelBuilder.Entity<Project>()
            .HasIndex(p => p.Slug)
            .IsUnique();

        // Índices compuestos para queries frecuentes
        modelBuilder.Entity<BacklogItem>()
            .HasIndex(b => new { b.ProjectId, b.Status });

        modelBuilder.Entity<DevSession>()
            .HasIndex(d => new { d.UserId, d.Status });

        modelBuilder.Entity<Execution>()
            .HasIndex(e => new { e.ProjectId, e.Status });

        // Configuración de JSONB (PostgreSQL)
        modelBuilder.Entity<CopilotExecution>()
            .Property(c => c.ArtifactsJson)
            .HasColumnType("jsonb");

        modelBuilder.Entity<BacklogItem>()
            .Property(b => b.MetadataJson)
            .HasColumnType("jsonb");

        // Relaciones y delete behaviors
        modelBuilder.Entity<DevSession>()
            .HasMany(d => d.Messages)
            .WithOne(m => m.Session)
            .HasForeignKey(m => m.SessionId)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder.Entity<CopilotExecution>()
            .HasMany(c => c.Steps)
            .WithOne(s => s.Execution)
            .HasForeignKey(s => s.ExecutionId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}

9.2 Migraciones

# Crear nueva migración
cd Orchestrator/src/Orchestrator.Api
dotnet ef migrations add NombreMigracion --project ../../Shared/Shared.Admin

# Aplicar migraciones
dotnet ef database update --project ../../Shared/Shared.Admin

# Generar script SQL
dotnet ef migrations script --project ../../Shared/Shared.Admin --idempotent

9.3 Patrones de Acceso a Datos

// ✅ Patrón correcto: Crear scope para operaciones background
public class CopilotService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public async Task ExecuteStepsAsync(Guid executionId)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<NexusDbContext>();

        // Ahora podemos usar db de forma segura en background
        var execution = await db.CopilotExecutions
            .Include(e => e.Steps)
            .FirstOrDefaultAsync(e => e.Id == executionId);

        // ... procesamiento
    }
}

// ✅ Patrón correcto: Proyección para listados
public async Task<List<BacklogItemListDto>> GetListAsync()
{
    return await _db.BacklogItems
        .Where(b => b.ProjectId == projectId)
        .OrderByDescending(b => b.CreatedAt)
        .Select(b => new BacklogItemListDto(
            b.Id,
            b.Title,
            b.Status,
            b.Priority,
            b.ItemType,
            b.AICategory,
            b.EstimatedEffort,
            b.CreatedAt,
            b.UpdatedAt
        ))
        .ToListAsync();
}

// ❌ Evitar: Cargar entidad completa para listados
// return await _db.BacklogItems.ToListAsync();

10. Patrones y Convenciones

10.1 Estructura de Archivos

Feature/
├── FeatureController.cs      # Endpoints HTTP
├── FeatureService.cs         # Lógica de negocio
├── FeatureDtos.cs           # DTOs
├── Feature.cs               # Entidad (en Entities/)
└── FeatureTests.cs          # Tests (en Tests/)

10.2 Convenciones de Código

// Servicios: Sufijo "Service"
public interface IBacklogService { }
public class BacklogService : IBacklogService { }

// DTOs: Sufijo "Dto", "Request", "Response"
public record BacklogItemDto { }
public record CreateBacklogItemRequest { }
public record BacklogListResponse { }

// Entidades: Sin sufijo, PascalCase
public class BacklogItem { }
public class DevSession { }

// Controllers: Sufijo "Controller"
[ApiController]
[Route("api/[controller]")]
public class BacklogController : ControllerBase { }

// Métodos async: Sufijo "Async"
public async Task<BacklogItem> CreateAsync(...)
public async Task DeleteAsync(Guid id)

// Métodos de streaming: Sufijo "StreamAsync"
public async Task SendMessageStreamAsync(...)

10.3 Inyección de Dependencias

// Program.cs - Registrar servicios
builder.Services.AddScoped<IBacklogService, BacklogService>();
builder.Services.AddScoped<IDevSessionService, DevSessionService>();
builder.Services.AddScoped<ICopilotService, CopilotService>();

// Servicios que necesitan HttpClient
builder.Services.AddHttpClient<IClickUpSyncService, ClickUpSyncService>(client =>
{
    client.DefaultRequestHeaders.Add("Authorization", "Bearer ...");
});

// Servicios singleton (cuidado con DbContext)
builder.Services.AddSingleton<IMemoryCache, MemoryCache>();

// Hosted services para background jobs
builder.Services.AddHostedService<MetricsCollectorWorker>();

10.4 Manejo de Errores

// Excepciones personalizadas
public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}

public class ValidationException : Exception
{
    public Dictionary<string, string[]> Errors { get; }
    public ValidationException(Dictionary<string, string[]> errors)
        : base("Validation failed")
    {
        Errors = errors;
    }
}

// Middleware de manejo de errores
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;

        var (statusCode, message) = exception switch
        {
            NotFoundException => (404, exception.Message),
            ValidationException ve => (400, JsonSerializer.Serialize(ve.Errors)),
            UnauthorizedAccessException => (401, "Unauthorized"),
            _ => (500, "Internal server error")
        };

        context.Response.StatusCode = statusCode;
        await context.Response.WriteAsJsonAsync(new { error = message });
    });
});

11. Flujos de Datos Principales

11.1 Flujo de DevSession + Streaming

sequenceDiagram
    participant UI as Blazor UI
    participant API as API Controller
    participant DS as DevSessionService
    participant HM as HistoryManager
    participant AI as Claude API
    participant DB as PostgreSQL
    participant SR as SignalR

    UI->>API: POST /api/dev-sessions/{id}/messages
    API->>DS: SendMessageStreamAsync()
    DS->>DB: Load session + messages
    DS->>HM: GetOptimizedHistoryAsync()
    HM->>DB: Get recent messages
    HM-->>DS: Optimized history + summary
    DS->>AI: Stream completion (SSE)

    loop Streaming
        AI-->>DS: Chunk
        DS-->>API: Chunk
        API-->>UI: SSE data
    end

    DS->>DB: Save user message
    DS->>DB: Save assistant message
    DS->>SR: Notify session subscribers

11.2 Flujo de Copilot Execution

stateDiagram-v2
    [*] --> Idle: Create
    Idle --> Planning: Start
    Planning --> AwaitingApproval: Plan generated
    Planning --> Failed: Error
    AwaitingApproval --> Executing: Approve
    AwaitingApproval --> Planning: Modify
    AwaitingApproval --> Cancelled: Cancel
    Executing --> Paused: Pause
    Executing --> Completed: All steps done
    Executing --> Failed: Max retries exceeded
    Paused --> Executing: Resume
    Paused --> Cancelled: Cancel
    Completed --> [*]
    Failed --> [*]
    Cancelled --> [*]

11.3 Flujo de Batch Processing

sequenceDiagram
    participant UI as UI
    participant API as API
    participant CBS as CopilotBatchService
    participant CS as CopilotService
    participant DB as DB
    participant SR as SignalR

    UI->>API: CreateBatch(backlogItemIds)
    API->>CBS: CreateBatchAsync()
    CBS->>DB: Create CopilotBatchExecution
    CBS-->>API: batch

    UI->>API: StartBatch(batchId)
    API->>CBS: StartBatchAsync()
    CBS->>DB: Get first item
    CBS->>CS: StartAsync(objective)

    loop For each item
        CS->>CS: Execute steps
        CS->>CBS: OnTaskCompletedAsync()
        CBS->>DB: Update item status
        CBS->>DB: Get next item
        CBS->>SR: BatchProgressUpdated
        alt Has next item
            CBS->>CS: StartAsync(next)
        else No more items
            CBS->>SR: BatchCompleted
        end
    end

12. Guía de Integración

12.1 Agregar Nuevo Endpoint

// 1. Crear DTO
public record CreateFeatureRequest(string Name, string Description);
public record FeatureDto(Guid Id, string Name, string Description, DateTime CreatedAt);

// 2. Crear/extender servicio
public interface IFeatureService
{
    Task<Feature> CreateAsync(CreateFeatureRequest request);
    Task<FeatureDto?> GetAsync(Guid id);
}

public class FeatureService : IFeatureService
{
    private readonly NexusDbContext _db;

    public async Task<Feature> CreateAsync(CreateFeatureRequest request)
    {
        var feature = new Feature
        {
            Name = request.Name,
            Description = request.Description
        };

        _db.Features.Add(feature);
        await _db.SaveChangesAsync();

        return feature;
    }
}

// 3. Crear controller
[ApiController]
[Route("api/[controller]")]
public class FeaturesController : ControllerBase
{
    private readonly IFeatureService _featureService;

    [HttpPost]
    public async Task<ActionResult<FeatureDto>> Create(CreateFeatureRequest request)
    {
        var feature = await _featureService.CreateAsync(request);
        return CreatedAtAction(nameof(Get), new { id = feature.Id }, feature);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<FeatureDto>> Get(Guid id)
    {
        var feature = await _featureService.GetAsync(id);
        if (feature == null) return NotFound();
        return Ok(feature);
    }
}

// 4. Registrar en Program.cs
builder.Services.AddScoped<IFeatureService, FeatureService>();

12.2 Agregar Notificación SignalR

// 1. Definir mensaje
public record FeatureCreatedMessage(Guid Id, string Name, DateTime CreatedAt);

// 2. Inyectar IHubContext en el servicio
public class FeatureService : IFeatureService
{
    private readonly IHubContext<AdminHub> _hubContext;

    public async Task<Feature> CreateAsync(CreateFeatureRequest request)
    {
        var feature = // ... crear feature

        // Notificar a suscriptores
        await _hubContext.Clients.Group("dashboard")
            .SendAsync("FeatureCreated", new FeatureCreatedMessage(
                feature.Id,
                feature.Name,
                feature.CreatedAt
            ));

        return feature;
    }
}

// 3. Suscribirse en Blazor
_hubConnection.On<FeatureCreatedMessage>("FeatureCreated", message =>
{
    _features.Insert(0, message);
    StateHasChanged();
});

12.3 Agregar Integración Externa

// 1. Crear servicio de integración
public interface IExternalService
{
    Task<ExternalData> FetchAsync(string id);
    Task PushAsync(LocalData data);
}

public class ExternalService : IExternalService
{
    private readonly HttpClient _httpClient;
    private readonly IConfiguration _config;

    public ExternalService(HttpClient httpClient, IConfiguration config)
    {
        _httpClient = httpClient;
        _config = config;

        _httpClient.BaseAddress = new Uri(config["External:BaseUrl"]!);
        _httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", config["External:ApiKey"]);
    }

    public async Task<ExternalData> FetchAsync(string id)
    {
        var response = await _httpClient.GetAsync($"/api/data/{id}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<ExternalData>();
    }
}

// 2. Registrar con HttpClient factory
builder.Services.AddHttpClient<IExternalService, ExternalService>();

// 3. Configurar en appsettings.json
{
    "External": {
        "BaseUrl": "https://api.external.com",
        "ApiKey": "..." // Usar secrets en producción
    }
}

13. Testing

13.1 Estructura de Tests

Orchestrator.Tests/
├── Unit/
│   ├── Services/
│   │   ├── BacklogServiceTests.cs
│   │   ├── DevSessionServiceTests.cs
│   │   └── CopilotServiceTests.cs
│   └── Controllers/
│       └── BacklogControllerTests.cs
├── Integration/
│   ├── ApiTests/
│   │   └── BacklogApiTests.cs
│   └── DatabaseTests/
│       └── BacklogRepositoryTests.cs
└── Fixtures/
    ├── TestDbContext.cs
    └── TestDataFactory.cs

13.2 Ejemplo de Test Unitario

public class BacklogServiceTests
{
    private readonly Mock<NexusDbContext> _mockDb;
    private readonly Mock<IClickUpSyncService> _mockClickUp;
    private readonly BacklogService _service;

    public BacklogServiceTests()
    {
        _mockDb = new Mock<NexusDbContext>();
        _mockClickUp = new Mock<IClickUpSyncService>();
        _service = new BacklogService(_mockDb.Object, _mockClickUp.Object);
    }

    [Fact]
    public async Task CreateAsync_ValidRequest_CreatesBacklogItem()
    {
        // Arrange
        var request = new CreateBacklogItemRequest(
            Title: "Test item",
            Description: "Test description",
            ItemType: "bug",
            Priority: "high",
            ProjectId: Guid.NewGuid()
        );

        var mockSet = new Mock<DbSet<BacklogItem>>();
        _mockDb.Setup(d => d.BacklogItems).Returns(mockSet.Object);

        // Act
        var result = await _service.CreateAsync(request, Guid.NewGuid());

        // Assert
        Assert.NotNull(result);
        Assert.Equal(request.Title, result.Title);
        Assert.Equal("new", result.Status);
        mockSet.Verify(s => s.Add(It.IsAny<BacklogItem>()), Times.Once);
    }

    [Fact]
    public async Task ConvertToTaskAsync_ValidItem_CreatesScheduledTask()
    {
        // Arrange
        var backlogItem = new BacklogItem
        {
            Id = Guid.NewGuid(),
            Title = "Test item",
            ProjectId = Guid.NewGuid()
        };

        // ... setup mocks

        // Act
        var task = await _service.ConvertToTaskAsync(new ConvertToTaskRequest(
            backlogItem.Id,
            AgentId: "code-assistant",
            ProcedureId: null,
            ExecuteImmediately: false
        ));

        // Assert
        Assert.NotNull(task);
        Assert.Equal(backlogItem.Id, task.SourceBacklogItemId);
    }
}

13.3 Ejemplo de Test de Integración

public class BacklogApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly WebApplicationFactory<Program> _factory;

    public BacklogApiTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetBacklog_ReturnsOk()
    {
        // Act
        var response = await _client.GetAsync("/api/backlog?projectId=...");

        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadFromJsonAsync<BacklogListResponse>();
        Assert.NotNull(content);
    }

    [Fact]
    public async Task CreateBacklogItem_ValidRequest_ReturnsCreated()
    {
        // Arrange
        var request = new CreateBacklogItemRequest(
            Title: "Integration test item",
            Description: "Test",
            ItemType: "feature",
            Priority: "medium",
            ProjectId: Guid.Parse("...")
        );

        // Act
        var response = await _client.PostAsJsonAsync("/api/backlog", request);

        // Assert
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
    }
}

14. Troubleshooting

14.1 Problemas Comunes

Problema Causa Solución
DbContext disposed Uso de DbContext después de que el scope terminó Usar IServiceScopeFactory para crear nuevo scope
Deadlock en SignalR Llamar Clients.*.SendAsync dentro de lock Mover la notificación fuera del lock
Token timeout Respuesta muy larga de Claude Aumentar TimeoutMinutes en CopilotExecution
Memory leak No disponer HubConnection Implementar IAsyncDisposable
Concurrent update Múltiples updates al mismo registro Usar SemaphoreSlim por entidad

14.2 Logs y Debugging

// Habilitar logs detallados en appsettings.Development.json
{
    "Serilog": {
        "MinimumLevel": {
            "Default": "Debug",
            "Override": {
                "Microsoft.EntityFrameworkCore": "Warning",
                "Microsoft.AspNetCore": "Information"
            }
        }
    }
}

// Agregar contexto a logs
Log.ForContext("SessionId", sessionId)
   .ForContext("UserId", userId)
   .Information("Processing message for session");

// Query logs en PostgreSQL
SELECT * FROM "SystemLogs"
WHERE "SessionId" = 'guid'
ORDER BY "Timestamp" DESC
LIMIT 100;

14.3 Health Checks

// Endpoint de health check
GET /health

// Respuesta
{
    "status": "Healthy",
    "checks": {
        "database": "Healthy",
        "signalr": "Healthy",
        "claude_api": "Healthy"
    }
}

Apéndice A: Referencias Rápidas

Variables de Entorno

Variable Descripción Ejemplo
ConnectionStrings__NexusDb PostgreSQL connection string Host=localhost;Database=nexus;...
Anthropic__ApiKey API key de Claude sk-ant-...
ClickUp__ApiToken Token de ClickUp pk_...
Sentry__Dsn DSN de Sentry https://...@sentry.io/...

Puertos por Defecto

Servicio Puerto URL
Orchestrator.Api 5000 http://localhost:5000
Orchestrator.Admin 5001 http://localhost:5001
PostgreSQL 5432 -
RabbitMQ 5672/15672 http://localhost:15672

Comandos Útiles

# Iniciar API
cd Orchestrator/src/Orchestrator.Api && dotnet run

# Iniciar Admin UI
cd Orchestrator/src/Orchestrator.Admin && dotnet run

# Ejecutar tests
dotnet test Orchestrator.Tests/

# Aplicar migraciones
dotnet ef database update --project Shared/Shared.Admin

# Generar migración
dotnet ef migrations add MigrationName --project Shared/Shared.Admin

Historial de Cambios

Versión Fecha Cambios
1.0 2026-02-01 Versión inicial

Documento generado para el equipo de desarrollo de Calmia Nexus