using ClaudeDo.Data; using ClaudeDo.Data.Git; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Agents; using ClaudeDo.Worker.Config; using ClaudeDo.Worker.External; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Lifecycle; using ClaudeDo.Worker.Planning; using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.State; using ClaudeDo.Worker.Prime; using ClaudeDo.Worker.Worktrees; using Microsoft.EntityFrameworkCore; var cfg = WorkerConfig.Load(); var builder = WebApplication.CreateBuilder(args); // When launched by the Windows SCM, speak the Service Control Protocol so SCM // doesn't think we crashed (~30s timeout). No-op when running interactively. builder.Host.UseWindowsService(o => o.ServiceName = "ClaudeDoWorker"); builder.Services.AddDbContextFactory(opt => opt.UseSqlite($"Data Source={cfg.DbPath}")); builder.Services.AddSingleton(cfg); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddSignalR().AddJsonProtocol(options => { options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); }); // Runner stack. builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Queue dispatch primitives. QueueWaker holds the wake semaphore; the queue picker // performs atomic Queued→Running claim. Both injected into the state service so it // can wake the dispatcher without depending on QueueService directly. builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddSingleton(); builder.Services.AddSingleton>(sp => () => sp.GetRequiredService()); builder.Services.AddSingleton(sp => new TaskStateService( sp.GetRequiredService>(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>())); // Agent file management. var agentsDir = Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "agents"); Directory.CreateDirectory(agentsDir); builder.Services.AddSingleton(new AgentFileService(agentsDir)); var defaultAgentsBundleDir = Path.Combine(AppContext.BaseDirectory, "DefaultAgents"); builder.Services.AddSingleton(sp => new DefaultAgentSeeder( defaultAgentsBundleDir, agentsDir, sp.GetService>())); // Override slot owns RunNow / ContinueTask. Queue slot is the BackgroundService. builder.Services.AddSingleton(); // Prime Claude builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddSingleton(); builder.Services.AddSingleton(PrimeSchedulerOptions.Default); builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddHostedService(); // QueueService: singleton + hosted service (same instance). builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); // Planning session services. var planningSessionsDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".todo-app", "planning-sessions"); builder.Services.AddSingleton(sp => new PlanningSessionManager( sp.GetRequiredService>(), sp.GetRequiredService(), cfg, sp.GetRequiredService(), sp.GetRequiredService(), planningSessionsDir)); builder.Services.AddSingleton(sp => new WindowsTerminalPlanningLauncher("wt.exe", cfg.ClaudeBin)); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddScoped(sp => sp.GetRequiredService>().CreateDbContext()); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddMcpServer() .WithHttpTransport() .WithTools(); // Loopback-only bind. Firewall is irrelevant for 127.0.0.1. builder.WebHost.UseUrls($"http://127.0.0.1:{cfg.SignalRPort}"); var app = builder.Build(); using (var scope = app.Services.CreateScope()) { ClaudeDoDbContext.MigrateAndConfigure( scope.ServiceProvider.GetRequiredService()); } try { var seeder = app.Services.GetRequiredService(); var seedResult = await seeder.SeedMissingAsync(); app.Logger.LogInformation( "Default agents seeded: {Copied} copied, {Skipped} already present", seedResult.Copied, seedResult.Skipped); } catch (Exception ex) { app.Logger.LogWarning(ex, "Default agent seeding failed"); } app.UseMiddleware(); app.MapHub("/hub"); app.MapMcp("/mcp"); // Claude CLI preflight: fail fast if the configured binary is unreachable or non-zero. // Skippable via CLAUDEDO_SKIP_CLI_PREFLIGHT=1 for environments without the CLI (e.g. tests). if (Environment.GetEnvironmentVariable("CLAUDEDO_SKIP_CLI_PREFLIGHT") != "1") { var preflight = await ClaudeCliPreflight.CheckAsync(cfg.ClaudeBin); if (!preflight.Ok) { app.Logger.LogCritical( "Claude CLI preflight failed (bin: '{Bin}', exit: {Exit}): {Error}. " + "Fix `claude_bin` in worker.config.json or set CLAUDEDO_SKIP_CLI_PREFLIGHT=1 to bypass.", cfg.ClaudeBin, preflight.ExitCode, preflight.Error); Environment.Exit(1); } app.Logger.LogInformation("Claude CLI preflight OK: {Version}", preflight.Version); } app.Logger.LogInformation("ClaudeDo.Worker listening on http://127.0.0.1:{Port} (db: {Db})", cfg.SignalRPort, cfg.DbPath); // Build the external MCP endpoint as a separate WebApplication on its own port. // Rationale: ModelContextProtocol.AspNetCore registers one server per DI container, // so we need a second app to expose a different tool set under different auth. // Shared singletons (QueueService, HubBroadcaster, WorkerConfig, db factory) are // injected by instance so both apps operate on the same runtime state. WebApplication? externalApp = null; if (cfg.ExternalMcpPort > 0) { var externalBuilder = WebApplication.CreateBuilder(); externalBuilder.Services.AddSingleton(cfg); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService>()); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); externalBuilder.Services.AddScoped(sp => sp.GetRequiredService>().CreateDbContext()); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddMcpServer() .WithHttpTransport() .WithTools(); externalBuilder.WebHost.UseUrls($"http://127.0.0.1:{cfg.ExternalMcpPort}"); externalApp = externalBuilder.Build(); externalApp.UseMiddleware(); externalApp.MapMcp("/mcp"); externalApp.Logger.LogInformation( "ClaudeDo.Worker external MCP listening on http://127.0.0.1:{Port} (auth: {Auth})", cfg.ExternalMcpPort, string.IsNullOrEmpty(cfg.ExternalMcpApiKey) ? "loopback-only" : "X-ClaudeDo-Key"); } if (externalApp is null) { await app.RunAsync(); } else { await Task.WhenAll(app.RunAsync(), externalApp.RunAsync()); }