using System.Security.Cryptography; using System.Text; using System.Text.Json; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Planning; public sealed class PlanningSessionManager { private const string McpServerUrl = "http://127.0.0.1:47821/mcp"; private readonly TaskRepository _tasks; private readonly ListRepository _lists; private readonly string _rootDirectory; public PlanningSessionManager(TaskRepository tasks, ListRepository lists, string rootDirectory) { _tasks = tasks; _lists = lists; _rootDirectory = rootDirectory; } public async Task StartAsync(string taskId, CancellationToken ct) { var task = await _tasks.GetByIdAsync(taskId, ct) ?? throw new InvalidOperationException($"Task {taskId} not found."); if (task.ParentTaskId is not null) throw new InvalidOperationException("Cannot start a planning session on a child task."); if (task.Status != TaskStatus.Manual) throw new InvalidOperationException($"Task is in status {task.Status}; only Manual can start planning."); var token = GenerateToken(); _ = await _tasks.SetPlanningStartedAsync(taskId, token, ct) ?? throw new InvalidOperationException("Failed to transition task to Planning."); var sessionDir = Path.Combine(_rootDirectory, taskId); Directory.CreateDirectory(sessionDir); var files = new PlanningSessionFiles( sessionDir, Path.Combine(sessionDir, "mcp.json"), Path.Combine(sessionDir, "system-prompt.md"), Path.Combine(sessionDir, "initial-prompt.txt")); await File.WriteAllTextAsync(files.McpConfigPath, BuildMcpConfigJson(token), ct); await File.WriteAllTextAsync(files.SystemPromptPath, BuildSystemPrompt(), ct); await File.WriteAllTextAsync(files.InitialPromptPath, BuildInitialPrompt(task), ct); var list = await _lists.GetByIdAsync(task.ListId, ct) ?? throw new InvalidOperationException($"List {task.ListId} not found."); return new PlanningSessionStartContext(taskId, list.WorkingDir, files); } private static string GenerateToken() { var bytes = RandomNumberGenerator.GetBytes(32); return Convert.ToBase64String(bytes) .Replace('+', '-') .Replace('/', '_') .TrimEnd('='); } private static string BuildMcpConfigJson(string token) { var payload = new { mcpServers = new { claudedo = new { type = "http", url = McpServerUrl, headers = new Dictionary { ["Authorization"] = $"Bearer {token}" } } } }; return JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }); } private static string BuildSystemPrompt() => """ You are a planning assistant for ClaudeDo. Your role is to help break down a task into smaller, actionable subtasks. Use the available MCP tools (mcp__claudedo__*) to create child tasks. When you are done planning, finalize the session. Be concise and focused. Each subtask should be independently executable. """; private static string BuildInitialPrompt(TaskEntity task) { var sb = new StringBuilder(); sb.AppendLine($"# Task: {task.Title}"); if (!string.IsNullOrWhiteSpace(task.Description)) { sb.AppendLine(); sb.AppendLine(task.Description); } sb.AppendLine(); sb.AppendLine("---"); sb.AppendLine(); sb.AppendLine("Please analyze this task and break it down into concrete subtasks."); return sb.ToString(); } }