diff --git a/docs/superpowers/plans/2026-04-16-efcore-migration.md b/docs/superpowers/plans/2026-04-16-efcore-migration.md
new file mode 100644
index 0000000..b5fee35
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-16-efcore-migration.md
@@ -0,0 +1,1722 @@
+# EF Core Migration Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Replace the raw ADO.NET data layer with Entity Framework Core and LINQ queries.
+
+**Architecture:** Single `ClaudeDoDbContext` in `ClaudeDo.Data` with Fluent API configuration. All 6 repositories rewritten to LINQ. EF Core migrations replace `schema.sql` and `SchemaInitializer`. Atomic queue claim kept as `FromSqlRaw`.
+
+**Tech Stack:** .NET 8.0, Microsoft.EntityFrameworkCore.Sqlite 8.x, EF Core Migrations
+
+**Spec:** `docs/superpowers/specs/2026-04-16-efcore-migration-design.md`
+
+---
+
+## File Structure
+
+### New files
+- `src/ClaudeDo.Data/ClaudeDoDbContext.cs` — DbContext with DbSets and `OnModelCreating`
+- `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs` — Fluent API for TaskEntity
+- `src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs` — Fluent API for ListEntity
+- `src/ClaudeDo.Data/Configuration/TagEntityConfiguration.cs` — Fluent API for TagEntity
+- `src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs` — Fluent API for ListConfigEntity
+- `src/ClaudeDo.Data/Configuration/WorktreeEntityConfiguration.cs` — Fluent API for WorktreeEntity
+- `src/ClaudeDo.Data/Configuration/TaskRunEntityConfiguration.cs` — Fluent API for TaskRunEntity
+- `src/ClaudeDo.Data/Configuration/SubtaskEntityConfiguration.cs` — Fluent API for SubtaskEntity
+- `src/ClaudeDo.Data/Migrations/` — Generated by `dotnet ef migrations add`
+
+### Modified files
+- `src/ClaudeDo.Data/ClaudeDo.Data.csproj` — Swap packages
+- `src/ClaudeDo.Data/Models/TaskEntity.cs` — Add navigation properties
+- `src/ClaudeDo.Data/Models/ListEntity.cs` — Add navigation properties
+- `src/ClaudeDo.Data/Models/TagEntity.cs` — Add navigation properties
+- `src/ClaudeDo.Data/Models/TaskRunEntity.cs` — Add navigation property
+- `src/ClaudeDo.Data/Models/SubtaskEntity.cs` — Add navigation property
+- `src/ClaudeDo.Data/Models/WorktreeEntity.cs` — Add navigation property
+- `src/ClaudeDo.Data/Models/ListConfigEntity.cs` — Add navigation property
+- `src/ClaudeDo.Data/Repositories/TagRepository.cs` — Rewrite to EF Core
+- `src/ClaudeDo.Data/Repositories/SubtaskRepository.cs` — Rewrite to EF Core
+- `src/ClaudeDo.Data/Repositories/WorktreeRepository.cs` — Rewrite to EF Core
+- `src/ClaudeDo.Data/Repositories/ListRepository.cs` — Rewrite to EF Core
+- `src/ClaudeDo.Data/Repositories/TaskRunRepository.cs` — Rewrite to EF Core
+- `src/ClaudeDo.Data/Repositories/TaskRepository.cs` — Rewrite to EF Core
+- `src/ClaudeDo.App/Program.cs` — EF Core DI registration
+- `src/ClaudeDo.Worker/Program.cs` — EF Core DI registration
+- `src/ClaudeDo.Worker/Services/QueueService.cs` — Adapt to scoped repos via IDbContextFactory
+- `src/ClaudeDo.Worker/Services/StaleTaskRecovery.cs` — Adapt to scoped repos via IDbContextFactory
+- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — Adapt to scoped repos via IDbContextFactory
+- `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` — Adapt to scoped repos via IDbContextFactory
+- `src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs` — Adapt to IDbContextFactory
+- `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs` — Adapt to IDbContextFactory
+- `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs` — Adapt to IDbContextFactory
+- `src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs` — Adapt to IDbContextFactory
+- `tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj` — Swap packages
+- `tests/ClaudeDo.Worker.Tests/Infrastructure/DbFixture.cs` — EF Core fixture
+
+### Deleted files
+- `src/ClaudeDo.Data/SqliteConnectionFactory.cs`
+- `src/ClaudeDo.Data/SchemaInitializer.cs`
+- `schema/schema.sql`
+
+---
+
+### Task 1: Update Package References
+
+**Files:**
+- Modify: `src/ClaudeDo.Data/ClaudeDo.Data.csproj`
+- Modify: `tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj`
+
+- [ ] **Step 1: Update ClaudeDo.Data.csproj**
+
+Replace package references and remove the embedded resource:
+
+```xml
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+```
+
+- [ ] **Step 2: Update Worker.Tests.csproj**
+
+Replace `Microsoft.Data.Sqlite` with EF Core:
+
+```xml
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 3: Verify packages restore**
+
+Run: `dotnet restore ClaudeDo.slnx`
+Expected: Restore succeeds (build will fail — repositories still reference old types).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/ClaudeDo.Data/ClaudeDo.Data.csproj tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj
+git commit -m "chore(data): swap Microsoft.Data.Sqlite for EF Core packages"
+```
+
+---
+
+### Task 2: Add Navigation Properties to Models
+
+**Files:**
+- Modify: `src/ClaudeDo.Data/Models/TaskEntity.cs`
+- Modify: `src/ClaudeDo.Data/Models/ListEntity.cs`
+- Modify: `src/ClaudeDo.Data/Models/TagEntity.cs`
+- Modify: `src/ClaudeDo.Data/Models/WorktreeEntity.cs`
+- Modify: `src/ClaudeDo.Data/Models/ListConfigEntity.cs`
+- Modify: `src/ClaudeDo.Data/Models/TaskRunEntity.cs`
+- Modify: `src/ClaudeDo.Data/Models/SubtaskEntity.cs`
+
+- [ ] **Step 1: Update TaskEntity.cs**
+
+Add navigation properties at the end of the class, before the closing brace:
+
+```csharp
+namespace ClaudeDo.Data.Models;
+
+public enum TaskStatus
+{
+ Manual,
+ Queued,
+ Running,
+ Done,
+ Failed,
+}
+
+public sealed class TaskEntity
+{
+ public required string Id { get; init; }
+ public required string ListId { get; init; }
+ public required string Title { get; set; }
+ public string? Description { get; set; }
+ public TaskStatus Status { get; set; } = TaskStatus.Manual;
+ public DateTime? ScheduledFor { get; set; }
+ public string? Result { get; set; }
+ public string? LogPath { get; set; }
+ public required DateTime CreatedAt { get; init; }
+ public DateTime? StartedAt { get; set; }
+ public DateTime? FinishedAt { get; set; }
+ public string CommitType { get; set; } = "chore";
+ public string? Model { get; set; }
+ public string? SystemPrompt { get; set; }
+ public string? AgentPath { get; set; }
+
+ // Navigation properties
+ public ListEntity List { get; set; } = null!;
+ public WorktreeEntity? Worktree { get; set; }
+ public ICollection Tags { get; set; } = new List();
+ public ICollection Runs { get; set; } = new List();
+ public ICollection Subtasks { get; set; } = new List();
+}
+```
+
+- [ ] **Step 2: Update ListEntity.cs**
+
+```csharp
+namespace ClaudeDo.Data.Models;
+
+public sealed class ListEntity
+{
+ public required string Id { get; init; }
+ public required string Name { get; set; }
+ public required DateTime CreatedAt { get; init; }
+ public string? WorkingDir { get; set; }
+ public string DefaultCommitType { get; set; } = "chore";
+
+ // Navigation properties
+ public ListConfigEntity? Config { get; set; }
+ public ICollection Tasks { get; set; } = new List();
+ public ICollection Tags { get; set; } = new List();
+}
+```
+
+- [ ] **Step 3: Update TagEntity.cs**
+
+```csharp
+namespace ClaudeDo.Data.Models;
+
+public sealed class TagEntity
+{
+ public long Id { get; init; }
+ public required string Name { get; set; }
+
+ // Navigation properties
+ public ICollection Lists { get; set; } = new List();
+ public ICollection Tasks { get; set; } = new List();
+}
+```
+
+- [ ] **Step 4: Update WorktreeEntity.cs**
+
+```csharp
+namespace ClaudeDo.Data.Models;
+
+public enum WorktreeState
+{
+ Active,
+ Merged,
+ Discarded,
+ Kept,
+}
+
+public sealed class WorktreeEntity
+{
+ public required string TaskId { get; init; }
+ public required string Path { get; set; }
+ public required string BranchName { get; set; }
+ public required string BaseCommit { get; set; }
+ public string? HeadCommit { get; set; }
+ public string? DiffStat { get; set; }
+ public WorktreeState State { get; set; } = WorktreeState.Active;
+ public required DateTime CreatedAt { get; init; }
+
+ // Navigation property
+ public TaskEntity Task { get; set; } = null!;
+}
+```
+
+- [ ] **Step 5: Update ListConfigEntity.cs**
+
+```csharp
+namespace ClaudeDo.Data.Models;
+
+public sealed class ListConfigEntity
+{
+ public required string ListId { get; init; }
+ public string? Model { get; set; }
+ public string? SystemPrompt { get; set; }
+ public string? AgentPath { get; set; }
+
+ // Navigation property
+ public ListEntity List { get; set; } = null!;
+}
+```
+
+- [ ] **Step 6: Update TaskRunEntity.cs**
+
+```csharp
+namespace ClaudeDo.Data.Models;
+
+public sealed class TaskRunEntity
+{
+ public required string Id { get; init; }
+ public required string TaskId { get; init; }
+ public required int RunNumber { get; init; }
+ public string? SessionId { get; set; }
+ public required bool IsRetry { get; init; }
+ public required string Prompt { get; init; }
+ public string? ResultMarkdown { get; set; }
+ public string? StructuredOutputJson { get; set; }
+ public string? ErrorMarkdown { get; set; }
+ public int? ExitCode { get; set; }
+ public int? TurnCount { get; set; }
+ public int? TokensIn { get; set; }
+ public int? TokensOut { get; set; }
+ public string? LogPath { get; set; }
+ public DateTime? StartedAt { get; set; }
+ public DateTime? FinishedAt { get; set; }
+
+ // Navigation property
+ public TaskEntity Task { get; set; } = null!;
+}
+```
+
+- [ ] **Step 7: Update SubtaskEntity.cs**
+
+```csharp
+namespace ClaudeDo.Data.Models;
+
+public sealed class SubtaskEntity
+{
+ public required string Id { get; init; }
+ public required string TaskId { get; init; }
+ public required string Title { get; set; }
+ public bool Completed { get; set; }
+ public int OrderNum { get; set; }
+ public required DateTime CreatedAt { get; init; }
+
+ // Navigation property
+ public TaskEntity Task { get; set; } = null!;
+}
+```
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add src/ClaudeDo.Data/Models/
+git commit -m "feat(data): add navigation properties to all entity models"
+```
+
+---
+
+### Task 3: Create ClaudeDoDbContext and Fluent API Configurations
+
+**Files:**
+- Create: `src/ClaudeDo.Data/ClaudeDoDbContext.cs`
+- Create: `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs`
+- Create: `src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs`
+- Create: `src/ClaudeDo.Data/Configuration/TagEntityConfiguration.cs`
+- Create: `src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs`
+- Create: `src/ClaudeDo.Data/Configuration/WorktreeEntityConfiguration.cs`
+- Create: `src/ClaudeDo.Data/Configuration/TaskRunEntityConfiguration.cs`
+- Create: `src/ClaudeDo.Data/Configuration/SubtaskEntityConfiguration.cs`
+
+- [ ] **Step 1: Create ClaudeDoDbContext.cs**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace ClaudeDo.Data;
+
+public class ClaudeDoDbContext : DbContext
+{
+ public ClaudeDoDbContext(DbContextOptions options) : base(options) { }
+
+ public DbSet Tasks => Set();
+ public DbSet Lists => Set();
+ public DbSet Tags => Set();
+ public DbSet ListConfigs => Set();
+ public DbSet Worktrees => Set();
+ public DbSet TaskRuns => Set();
+ public DbSet Subtasks => Set();
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(ClaudeDoDbContext).Assembly);
+ }
+}
+```
+
+- [ ] **Step 2: Create TaskEntityConfiguration.cs**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
+
+namespace ClaudeDo.Data.Configuration;
+
+public class TaskEntityConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("tasks");
+
+ builder.HasKey(t => t.Id);
+ builder.Property(t => t.Id).HasColumnName("id");
+ builder.Property(t => t.ListId).HasColumnName("list_id").IsRequired();
+ builder.Property(t => t.Title).HasColumnName("title").IsRequired();
+ builder.Property(t => t.Description).HasColumnName("description");
+ builder.Property(t => t.Status).HasColumnName("status").IsRequired()
+ .HasConversion(
+ v => v switch
+ {
+ TaskStatus.Manual => "manual",
+ TaskStatus.Queued => "queued",
+ TaskStatus.Running => "running",
+ TaskStatus.Done => "done",
+ TaskStatus.Failed => "failed",
+ _ => throw new ArgumentOutOfRangeException()
+ },
+ v => v switch
+ {
+ "manual" => TaskStatus.Manual,
+ "queued" => TaskStatus.Queued,
+ "running" => TaskStatus.Running,
+ "done" => TaskStatus.Done,
+ "failed" => TaskStatus.Failed,
+ _ => throw new ArgumentOutOfRangeException()
+ });
+ builder.Property(t => t.ScheduledFor).HasColumnName("scheduled_for");
+ builder.Property(t => t.Result).HasColumnName("result");
+ builder.Property(t => t.LogPath).HasColumnName("log_path");
+ builder.Property(t => t.CreatedAt).HasColumnName("created_at").IsRequired();
+ builder.Property(t => t.StartedAt).HasColumnName("started_at");
+ builder.Property(t => t.FinishedAt).HasColumnName("finished_at");
+ builder.Property(t => t.CommitType).HasColumnName("commit_type").IsRequired().HasDefaultValue("chore");
+ builder.Property(t => t.Model).HasColumnName("model");
+ builder.Property(t => t.SystemPrompt).HasColumnName("system_prompt");
+ builder.Property(t => t.AgentPath).HasColumnName("agent_path");
+
+ builder.HasOne(t => t.List)
+ .WithMany(l => l.Tasks)
+ .HasForeignKey(t => t.ListId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasOne(t => t.Worktree)
+ .WithOne(w => w.Task)
+ .HasForeignKey(w => w.TaskId);
+
+ builder.HasMany(t => t.Tags)
+ .WithMany(tag => tag.Tasks)
+ .UsingEntity("task_tags",
+ l => l.HasOne(typeof(TagEntity)).WithMany().HasForeignKey("tag_id").OnDelete(DeleteBehavior.Cascade),
+ r => r.HasOne(typeof(TaskEntity)).WithMany().HasForeignKey("task_id").OnDelete(DeleteBehavior.Cascade),
+ j =>
+ {
+ j.HasKey("task_id", "tag_id");
+ j.ToTable("task_tags");
+ });
+
+ builder.HasIndex(t => t.ListId).HasDatabaseName("idx_tasks_list_id");
+ builder.HasIndex(t => t.Status).HasDatabaseName("idx_tasks_status");
+ }
+}
+```
+
+- [ ] **Step 3: Create ListEntityConfiguration.cs**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ClaudeDo.Data.Configuration;
+
+public class ListEntityConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("lists");
+
+ builder.HasKey(l => l.Id);
+ builder.Property(l => l.Id).HasColumnName("id");
+ builder.Property(l => l.Name).HasColumnName("name").IsRequired();
+ builder.Property(l => l.CreatedAt).HasColumnName("created_at").IsRequired();
+ builder.Property(l => l.WorkingDir).HasColumnName("working_dir");
+ builder.Property(l => l.DefaultCommitType).HasColumnName("default_commit_type").IsRequired().HasDefaultValue("chore");
+
+ builder.HasOne(l => l.Config)
+ .WithOne(c => c.List)
+ .HasForeignKey(c => c.ListId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasMany(l => l.Tags)
+ .WithMany(tag => tag.Lists)
+ .UsingEntity("list_tags",
+ l => l.HasOne(typeof(TagEntity)).WithMany().HasForeignKey("tag_id").OnDelete(DeleteBehavior.Cascade),
+ r => r.HasOne(typeof(ListEntity)).WithMany().HasForeignKey("list_id").OnDelete(DeleteBehavior.Cascade),
+ j =>
+ {
+ j.HasKey("list_id", "tag_id");
+ j.ToTable("list_tags");
+ });
+ }
+}
+```
+
+- [ ] **Step 4: Create TagEntityConfiguration.cs**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ClaudeDo.Data.Configuration;
+
+public class TagEntityConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("tags");
+
+ builder.HasKey(t => t.Id);
+ builder.Property(t => t.Id).HasColumnName("id").ValueGeneratedOnAdd();
+ builder.Property(t => t.Name).HasColumnName("name").IsRequired();
+ builder.HasIndex(t => t.Name).IsUnique();
+
+ builder.HasData(
+ new TagEntity { Id = 1, Name = "agent" },
+ new TagEntity { Id = 2, Name = "manual" });
+ }
+}
+```
+
+- [ ] **Step 5: Create ListConfigEntityConfiguration.cs**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ClaudeDo.Data.Configuration;
+
+public class ListConfigEntityConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("list_config");
+
+ builder.HasKey(c => c.ListId);
+ builder.Property(c => c.ListId).HasColumnName("list_id");
+ builder.Property(c => c.Model).HasColumnName("model");
+ builder.Property(c => c.SystemPrompt).HasColumnName("system_prompt");
+ builder.Property(c => c.AgentPath).HasColumnName("agent_path");
+ }
+}
+```
+
+- [ ] **Step 6: Create WorktreeEntityConfiguration.cs**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ClaudeDo.Data.Configuration;
+
+public class WorktreeEntityConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("worktrees");
+
+ builder.HasKey(w => w.TaskId);
+ builder.Property(w => w.TaskId).HasColumnName("task_id");
+ builder.Property(w => w.Path).HasColumnName("path").IsRequired();
+ builder.Property(w => w.BranchName).HasColumnName("branch_name").IsRequired();
+ builder.Property(w => w.BaseCommit).HasColumnName("base_commit").IsRequired();
+ builder.Property(w => w.HeadCommit).HasColumnName("head_commit");
+ builder.Property(w => w.DiffStat).HasColumnName("diff_stat");
+ builder.Property(w => w.State).HasColumnName("state").IsRequired()
+ .HasDefaultValue(WorktreeState.Active)
+ .HasConversion(
+ v => v switch
+ {
+ WorktreeState.Active => "active",
+ WorktreeState.Merged => "merged",
+ WorktreeState.Discarded => "discarded",
+ WorktreeState.Kept => "kept",
+ _ => throw new ArgumentOutOfRangeException()
+ },
+ v => v switch
+ {
+ "active" => WorktreeState.Active,
+ "merged" => WorktreeState.Merged,
+ "discarded" => WorktreeState.Discarded,
+ "kept" => WorktreeState.Kept,
+ _ => throw new ArgumentOutOfRangeException()
+ });
+ builder.Property(w => w.CreatedAt).HasColumnName("created_at").IsRequired();
+ }
+}
+```
+
+- [ ] **Step 7: Create TaskRunEntityConfiguration.cs**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ClaudeDo.Data.Configuration;
+
+public class TaskRunEntityConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("task_runs");
+
+ builder.HasKey(r => r.Id);
+ builder.Property(r => r.Id).HasColumnName("id");
+ builder.Property(r => r.TaskId).HasColumnName("task_id").IsRequired();
+ builder.Property(r => r.RunNumber).HasColumnName("run_number").IsRequired();
+ builder.Property(r => r.SessionId).HasColumnName("session_id");
+ builder.Property(r => r.IsRetry).HasColumnName("is_retry").IsRequired().HasDefaultValue(false);
+ builder.Property(r => r.Prompt).HasColumnName("prompt").IsRequired();
+ builder.Property(r => r.ResultMarkdown).HasColumnName("result_markdown");
+ builder.Property(r => r.StructuredOutputJson).HasColumnName("structured_output");
+ builder.Property(r => r.ErrorMarkdown).HasColumnName("error_markdown");
+ builder.Property(r => r.ExitCode).HasColumnName("exit_code");
+ builder.Property(r => r.TurnCount).HasColumnName("turn_count");
+ builder.Property(r => r.TokensIn).HasColumnName("tokens_in");
+ builder.Property(r => r.TokensOut).HasColumnName("tokens_out");
+ builder.Property(r => r.LogPath).HasColumnName("log_path");
+ builder.Property(r => r.StartedAt).HasColumnName("started_at");
+ builder.Property(r => r.FinishedAt).HasColumnName("finished_at");
+
+ builder.HasOne(r => r.Task)
+ .WithMany(t => t.Runs)
+ .HasForeignKey(r => r.TaskId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasIndex(r => r.TaskId).HasDatabaseName("idx_task_runs_task_id");
+ }
+}
+```
+
+- [ ] **Step 8: Create SubtaskEntityConfiguration.cs**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ClaudeDo.Data.Configuration;
+
+public class SubtaskEntityConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("subtasks");
+
+ builder.HasKey(s => s.Id);
+ builder.Property(s => s.Id).HasColumnName("id");
+ builder.Property(s => s.TaskId).HasColumnName("task_id").IsRequired();
+ builder.Property(s => s.Title).HasColumnName("title").IsRequired();
+ builder.Property(s => s.Completed).HasColumnName("completed").IsRequired().HasDefaultValue(false);
+ builder.Property(s => s.OrderNum).HasColumnName("order_num").IsRequired();
+ builder.Property(s => s.CreatedAt).HasColumnName("created_at").IsRequired();
+
+ builder.HasOne(s => s.Task)
+ .WithMany(t => t.Subtasks)
+ .HasForeignKey(s => s.TaskId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder.HasIndex(s => s.TaskId).HasDatabaseName("idx_subtasks_task_id");
+ }
+}
+```
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add src/ClaudeDo.Data/ClaudeDoDbContext.cs src/ClaudeDo.Data/Configuration/
+git commit -m "feat(data): add ClaudeDoDbContext with Fluent API configurations"
+```
+
+---
+
+### Task 4: Rewrite TagRepository
+
+**Files:**
+- Modify: `src/ClaudeDo.Data/Repositories/TagRepository.cs`
+
+- [ ] **Step 1: Rewrite TagRepository to EF Core**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace ClaudeDo.Data.Repositories;
+
+public sealed class TagRepository
+{
+ private readonly ClaudeDoDbContext _context;
+
+ public TagRepository(ClaudeDoDbContext context) => _context = context;
+
+ public async Task> GetAllAsync(CancellationToken ct = default)
+ {
+ return await _context.Tags.OrderBy(t => t.Id).ToListAsync(ct);
+ }
+
+ public async Task GetOrCreateAsync(string name, CancellationToken ct = default)
+ {
+ var existing = await _context.Tags.FirstOrDefaultAsync(t => t.Name == name, ct);
+ if (existing is not null)
+ return existing.Id;
+
+ var tag = new TagEntity { Name = name };
+ _context.Tags.Add(tag);
+ await _context.SaveChangesAsync(ct);
+ return tag.Id;
+ }
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/ClaudeDo.Data/Repositories/TagRepository.cs
+git commit -m "refactor(data): rewrite TagRepository to EF Core LINQ"
+```
+
+---
+
+### Task 5: Rewrite SubtaskRepository
+
+**Files:**
+- Modify: `src/ClaudeDo.Data/Repositories/SubtaskRepository.cs`
+
+- [ ] **Step 1: Rewrite SubtaskRepository to EF Core**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace ClaudeDo.Data.Repositories;
+
+public sealed class SubtaskRepository
+{
+ private readonly ClaudeDoDbContext _context;
+
+ public SubtaskRepository(ClaudeDoDbContext context) => _context = context;
+
+ public async Task AddAsync(SubtaskEntity entity, CancellationToken ct = default)
+ {
+ _context.Subtasks.Add(entity);
+ await _context.SaveChangesAsync(ct);
+ }
+
+ public async Task> GetByTaskIdAsync(string taskId, CancellationToken ct = default)
+ {
+ return await _context.Subtasks
+ .Where(s => s.TaskId == taskId)
+ .OrderBy(s => s.OrderNum)
+ .ToListAsync(ct);
+ }
+
+ public async Task UpdateAsync(SubtaskEntity entity, CancellationToken ct = default)
+ {
+ _context.Subtasks.Update(entity);
+ await _context.SaveChangesAsync(ct);
+ }
+
+ public async Task DeleteAsync(string subtaskId, CancellationToken ct = default)
+ {
+ await _context.Subtasks.Where(s => s.Id == subtaskId).ExecuteDeleteAsync(ct);
+ }
+
+ public async Task DeleteByTaskIdAsync(string taskId, CancellationToken ct = default)
+ {
+ await _context.Subtasks.Where(s => s.TaskId == taskId).ExecuteDeleteAsync(ct);
+ }
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/ClaudeDo.Data/Repositories/SubtaskRepository.cs
+git commit -m "refactor(data): rewrite SubtaskRepository to EF Core LINQ"
+```
+
+---
+
+### Task 6: Rewrite WorktreeRepository
+
+**Files:**
+- Modify: `src/ClaudeDo.Data/Repositories/WorktreeRepository.cs`
+
+- [ ] **Step 1: Rewrite WorktreeRepository to EF Core**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace ClaudeDo.Data.Repositories;
+
+public sealed class WorktreeRepository
+{
+ private readonly ClaudeDoDbContext _context;
+
+ public WorktreeRepository(ClaudeDoDbContext context) => _context = context;
+
+ public async Task AddAsync(WorktreeEntity entity, CancellationToken ct = default)
+ {
+ _context.Worktrees.Add(entity);
+ await _context.SaveChangesAsync(ct);
+ }
+
+ public async Task GetByTaskIdAsync(string taskId, CancellationToken ct = default)
+ {
+ return await _context.Worktrees.FirstOrDefaultAsync(w => w.TaskId == taskId, ct);
+ }
+
+ public async Task UpdateHeadAsync(string taskId, string headCommit, string? diffStat, CancellationToken ct = default)
+ {
+ await _context.Worktrees
+ .Where(w => w.TaskId == taskId)
+ .ExecuteUpdateAsync(s => s
+ .SetProperty(w => w.HeadCommit, headCommit)
+ .SetProperty(w => w.DiffStat, diffStat), ct);
+ }
+
+ public async Task SetStateAsync(string taskId, WorktreeState state, CancellationToken ct = default)
+ {
+ await _context.Worktrees
+ .Where(w => w.TaskId == taskId)
+ .ExecuteUpdateAsync(s => s.SetProperty(w => w.State, state), ct);
+ }
+
+ public async Task DeleteAsync(string taskId, CancellationToken ct = default)
+ {
+ await _context.Worktrees.Where(w => w.TaskId == taskId).ExecuteDeleteAsync(ct);
+ }
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/ClaudeDo.Data/Repositories/WorktreeRepository.cs
+git commit -m "refactor(data): rewrite WorktreeRepository to EF Core LINQ"
+```
+
+---
+
+### Task 7: Rewrite ListRepository
+
+**Files:**
+- Modify: `src/ClaudeDo.Data/Repositories/ListRepository.cs`
+
+- [ ] **Step 1: Rewrite ListRepository to EF Core**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace ClaudeDo.Data.Repositories;
+
+public sealed class ListRepository
+{
+ private readonly ClaudeDoDbContext _context;
+
+ public ListRepository(ClaudeDoDbContext context) => _context = context;
+
+ public async Task AddAsync(ListEntity entity, CancellationToken ct = default)
+ {
+ _context.Lists.Add(entity);
+ await _context.SaveChangesAsync(ct);
+ }
+
+ public async Task UpdateAsync(ListEntity entity, CancellationToken ct = default)
+ {
+ _context.Lists.Update(entity);
+ await _context.SaveChangesAsync(ct);
+ }
+
+ public async Task DeleteAsync(string listId, CancellationToken ct = default)
+ {
+ await _context.Lists.Where(l => l.Id == listId).ExecuteDeleteAsync(ct);
+ }
+
+ public async Task GetByIdAsync(string listId, CancellationToken ct = default)
+ {
+ return await _context.Lists.FirstOrDefaultAsync(l => l.Id == listId, ct);
+ }
+
+ public async Task> GetAllAsync(CancellationToken ct = default)
+ {
+ return await _context.Lists.OrderBy(l => l.CreatedAt).ToListAsync(ct);
+ }
+
+ // Tag management via junction table
+
+ public async Task> GetTagsAsync(string listId, CancellationToken ct = default)
+ {
+ return await _context.Lists
+ .Where(l => l.Id == listId)
+ .SelectMany(l => l.Tags)
+ .ToListAsync(ct);
+ }
+
+ public async Task AddTagAsync(string listId, long tagId, CancellationToken ct = default)
+ {
+ var list = await _context.Lists.Include(l => l.Tags).FirstAsync(l => l.Id == listId, ct);
+ var tag = await _context.Tags.FindAsync([tagId], ct);
+ if (tag is not null && !list.Tags.Any(t => t.Id == tagId))
+ {
+ list.Tags.Add(tag);
+ await _context.SaveChangesAsync(ct);
+ }
+ }
+
+ public async Task RemoveTagAsync(string listId, long tagId, CancellationToken ct = default)
+ {
+ var list = await _context.Lists.Include(l => l.Tags).FirstAsync(l => l.Id == listId, ct);
+ var tag = list.Tags.FirstOrDefault(t => t.Id == tagId);
+ if (tag is not null)
+ {
+ list.Tags.Remove(tag);
+ await _context.SaveChangesAsync(ct);
+ }
+ }
+
+ // Config management (1:1 with list)
+
+ public async Task GetConfigAsync(string listId, CancellationToken ct = default)
+ {
+ return await _context.ListConfigs.FirstOrDefaultAsync(c => c.ListId == listId, ct);
+ }
+
+ public async Task UpsertConfigAsync(ListConfigEntity config, CancellationToken ct = default)
+ {
+ var existing = await _context.ListConfigs.FirstOrDefaultAsync(c => c.ListId == config.ListId, ct);
+ if (existing is null)
+ {
+ _context.ListConfigs.Add(config);
+ }
+ else
+ {
+ existing.Model = config.Model;
+ existing.SystemPrompt = config.SystemPrompt;
+ existing.AgentPath = config.AgentPath;
+ }
+ await _context.SaveChangesAsync(ct);
+ }
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/ClaudeDo.Data/Repositories/ListRepository.cs
+git commit -m "refactor(data): rewrite ListRepository to EF Core LINQ"
+```
+
+---
+
+### Task 8: Rewrite TaskRunRepository
+
+**Files:**
+- Modify: `src/ClaudeDo.Data/Repositories/TaskRunRepository.cs`
+
+- [ ] **Step 1: Rewrite TaskRunRepository to EF Core**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace ClaudeDo.Data.Repositories;
+
+public sealed class TaskRunRepository
+{
+ private readonly ClaudeDoDbContext _context;
+
+ public TaskRunRepository(ClaudeDoDbContext context) => _context = context;
+
+ public async Task AddAsync(TaskRunEntity entity, CancellationToken ct = default)
+ {
+ _context.TaskRuns.Add(entity);
+ await _context.SaveChangesAsync(ct);
+ }
+
+ public async Task UpdateAsync(TaskRunEntity entity, CancellationToken ct = default)
+ {
+ _context.TaskRuns.Update(entity);
+ await _context.SaveChangesAsync(ct);
+ }
+
+ public async Task GetByIdAsync(string id, CancellationToken ct = default)
+ {
+ return await _context.TaskRuns.FirstOrDefaultAsync(r => r.Id == id, ct);
+ }
+
+ public async Task> GetByTaskIdAsync(string taskId, CancellationToken ct = default)
+ {
+ return await _context.TaskRuns
+ .Where(r => r.TaskId == taskId)
+ .OrderBy(r => r.RunNumber)
+ .ToListAsync(ct);
+ }
+
+ public async Task GetLatestByTaskIdAsync(string taskId, CancellationToken ct = default)
+ {
+ return await _context.TaskRuns
+ .Where(r => r.TaskId == taskId)
+ .OrderByDescending(r => r.RunNumber)
+ .FirstOrDefaultAsync(ct);
+ }
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/ClaudeDo.Data/Repositories/TaskRunRepository.cs
+git commit -m "refactor(data): rewrite TaskRunRepository to EF Core LINQ"
+```
+
+---
+
+### Task 9: Rewrite TaskRepository
+
+**Files:**
+- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs`
+
+This is the most complex repository. The atomic queue claim stays as raw SQL.
+
+- [ ] **Step 1: Rewrite TaskRepository to EF Core**
+
+```csharp
+using ClaudeDo.Data.Models;
+using Microsoft.EntityFrameworkCore;
+using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
+
+namespace ClaudeDo.Data.Repositories;
+
+public sealed class TaskRepository
+{
+ private readonly ClaudeDoDbContext _context;
+
+ public TaskRepository(ClaudeDoDbContext context) => _context = context;
+
+ #region CRUD
+
+ public async Task AddAsync(TaskEntity entity, CancellationToken ct = default)
+ {
+ _context.Tasks.Add(entity);
+ await _context.SaveChangesAsync(ct);
+ }
+
+ public async Task UpdateAsync(TaskEntity entity, CancellationToken ct = default)
+ {
+ _context.Tasks.Update(entity);
+ await _context.SaveChangesAsync(ct);
+ }
+
+ public async Task DeleteAsync(string taskId, CancellationToken ct = default)
+ {
+ await _context.Tasks.Where(t => t.Id == taskId).ExecuteDeleteAsync(ct);
+ }
+
+ public async Task GetByIdAsync(string taskId, CancellationToken ct = default)
+ {
+ return await _context.Tasks.FirstOrDefaultAsync(t => t.Id == taskId, ct);
+ }
+
+ public async Task> GetByListIdAsync(string listId, CancellationToken ct = default)
+ {
+ return await _context.Tasks
+ .Where(t => t.ListId == listId)
+ .OrderByDescending(t => t.CreatedAt)
+ .ToListAsync(ct);
+ }
+
+ #endregion
+
+ #region Status transitions
+
+ public async Task MarkRunningAsync(string taskId, DateTime startedAt, CancellationToken ct = default)
+ {
+ await _context.Tasks
+ .Where(t => t.Id == taskId)
+ .ExecuteUpdateAsync(s => s
+ .SetProperty(t => t.Status, TaskStatus.Running)
+ .SetProperty(t => t.StartedAt, startedAt), ct);
+ }
+
+ public async Task MarkDoneAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct = default)
+ {
+ await _context.Tasks
+ .Where(t => t.Id == taskId)
+ .ExecuteUpdateAsync(s => s
+ .SetProperty(t => t.Status, TaskStatus.Done)
+ .SetProperty(t => t.FinishedAt, finishedAt)
+ .SetProperty(t => t.Result, result), ct);
+ }
+
+ public async Task MarkFailedAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct = default)
+ {
+ await _context.Tasks
+ .Where(t => t.Id == taskId)
+ .ExecuteUpdateAsync(s => s
+ .SetProperty(t => t.Status, TaskStatus.Failed)
+ .SetProperty(t => t.FinishedAt, finishedAt)
+ .SetProperty(t => t.Result, result), ct);
+ }
+
+ public async Task SetLogPathAsync(string taskId, string logPath, CancellationToken ct = default)
+ {
+ await _context.Tasks
+ .Where(t => t.Id == taskId)
+ .ExecuteUpdateAsync(s => s.SetProperty(t => t.LogPath, logPath), ct);
+ }
+
+ public async Task FlipAllRunningToFailedAsync(CancellationToken ct = default)
+ {
+ await _context.Tasks
+ .Where(t => t.Status == TaskStatus.Running)
+ .ExecuteUpdateAsync(s => s
+ .SetProperty(t => t.Status, TaskStatus.Failed)
+ .SetProperty(t => t.FinishedAt, DateTime.UtcNow), ct);
+ }
+
+ #endregion
+
+ #region Tags
+
+ public async Task AddTagAsync(string taskId, long tagId, CancellationToken ct = default)
+ {
+ var task = await _context.Tasks.Include(t => t.Tags).FirstAsync(t => t.Id == taskId, ct);
+ var tag = await _context.Tags.FindAsync([tagId], ct);
+ if (tag is not null && !task.Tags.Any(t => t.Id == tagId))
+ {
+ task.Tags.Add(tag);
+ await _context.SaveChangesAsync(ct);
+ }
+ }
+
+ public async Task RemoveTagAsync(string taskId, long tagId, CancellationToken ct = default)
+ {
+ var task = await _context.Tasks.Include(t => t.Tags).FirstAsync(t => t.Id == taskId, ct);
+ var tag = task.Tags.FirstOrDefault(t => t.Id == tagId);
+ if (tag is not null)
+ {
+ task.Tags.Remove(tag);
+ await _context.SaveChangesAsync(ct);
+ }
+ }
+
+ public async Task> GetTagsAsync(string taskId, CancellationToken ct = default)
+ {
+ return await _context.Tasks
+ .Where(t => t.Id == taskId)
+ .SelectMany(t => t.Tags)
+ .ToListAsync(ct);
+ }
+
+ public async Task> GetEffectiveTagsAsync(string taskId, CancellationToken ct = default)
+ {
+ var taskTags = _context.Tasks
+ .Where(t => t.Id == taskId)
+ .SelectMany(t => t.Tags);
+ var listTags = _context.Tasks
+ .Where(t => t.Id == taskId)
+ .SelectMany(t => t.List.Tags);
+ return await taskTags.Union(listTags).Distinct().ToListAsync(ct);
+ }
+
+ #endregion
+
+ #region Queue selection
+
+ public async Task GetNextQueuedAgentTaskAsync(DateTime now, CancellationToken ct = default)
+ {
+ // Atomic queue claim: UPDATE + SELECT in one statement to prevent TOCTOU races.
+ // Not expressible in LINQ — kept as raw SQL.
+ var result = await _context.Tasks.FromSqlRaw("""
+ UPDATE tasks SET status = 'running'
+ WHERE id = (
+ SELECT t.id FROM tasks t
+ JOIN task_tags tt ON tt.task_id = t.id
+ JOIN tags tg ON tg.id = tt.tag_id AND tg.name = 'agent'
+ WHERE t.status = 'queued'
+ AND (t.scheduled_for IS NULL OR t.scheduled_for <= {0})
+ ORDER BY t.created_at
+ LIMIT 1
+ )
+ RETURNING *
+ """, now.ToString("o")).ToListAsync(ct);
+
+ return result.FirstOrDefault();
+ }
+
+ #endregion
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/ClaudeDo.Data/Repositories/TaskRepository.cs
+git commit -m "refactor(data): rewrite TaskRepository to EF Core LINQ with raw SQL queue claim"
+```
+
+---
+
+### Task 10: Delete Old Infrastructure and Schema
+
+**Files:**
+- Delete: `src/ClaudeDo.Data/SqliteConnectionFactory.cs`
+- Delete: `src/ClaudeDo.Data/SchemaInitializer.cs`
+- Delete: `schema/schema.sql`
+
+- [ ] **Step 1: Delete SqliteConnectionFactory.cs**
+
+```bash
+rm src/ClaudeDo.Data/SqliteConnectionFactory.cs
+```
+
+- [ ] **Step 2: Delete SchemaInitializer.cs**
+
+```bash
+rm src/ClaudeDo.Data/SchemaInitializer.cs
+```
+
+- [ ] **Step 3: Delete schema.sql**
+
+```bash
+rm schema/schema.sql
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add -u src/ClaudeDo.Data/SqliteConnectionFactory.cs src/ClaudeDo.Data/SchemaInitializer.cs schema/schema.sql
+git commit -m "chore(data): remove raw ADO.NET infrastructure and schema.sql"
+```
+
+---
+
+### Task 11: Update DI Registration — Worker
+
+**Files:**
+- Modify: `src/ClaudeDo.Worker/Program.cs`
+
+- [ ] **Step 1: Rewrite Worker Program.cs DI section**
+
+Replace lines 1-28 with EF Core registration. The `SqliteConnectionFactory` and `SchemaInitializer` calls are replaced by `AddDbContext` and `Database.Migrate()`.
+
+The full updated `Program.cs`:
+
+```csharp
+using ClaudeDo.Data;
+using ClaudeDo.Data.Git;
+using ClaudeDo.Data.Repositories;
+using ClaudeDo.Worker.Config;
+using ClaudeDo.Worker.Hub;
+using ClaudeDo.Worker.Runner;
+using ClaudeDo.Worker.Services;
+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.AddSingleton(cfg);
+
+// EF Core DbContext — scoped lifetime (one context per scope/request).
+builder.Services.AddDbContext(opt =>
+ opt.UseSqlite($"Data Source={cfg.DbPath}"));
+
+// Repositories — scoped (they depend on the scoped DbContext).
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+builder.Services.AddHostedService();
+builder.Services.AddSignalR();
+
+// Runner stack.
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton(new AgentFileService(Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ ".todo-app", "agents")));
+
+// QueueService: singleton + hosted service (same instance).
+builder.Services.AddSingleton();
+builder.Services.AddHostedService(sp => sp.GetRequiredService());
+
+// 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();
+
+// Apply EF Core migrations at startup.
+using (var scope = app.Services.CreateScope())
+{
+ var db = scope.ServiceProvider.GetRequiredService();
+ db.Database.Migrate();
+}
+
+app.MapHub("/hub");
+
+app.Logger.LogInformation("ClaudeDo.Worker listening on http://127.0.0.1:{Port} (db: {Db})",
+ cfg.SignalRPort, cfg.DbPath);
+
+app.Run();
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/ClaudeDo.Worker/Program.cs
+git commit -m "refactor(worker): switch DI from raw ADO.NET to EF Core"
+```
+
+---
+
+### Task 12: Update DI Registration — App
+
+**Files:**
+- Modify: `src/ClaudeDo.App/Program.cs`
+
+- [ ] **Step 1: Rewrite App Program.cs DI section**
+
+The App uses `ServiceCollection` directly (no WebApplication builder). ViewModels are singletons that need database access — they take `IDbContextFactory` instead of repositories.
+
+```csharp
+using Avalonia;
+using ClaudeDo.Data;
+using ClaudeDo.Data.Git;
+using ClaudeDo.Data.Repositories;
+using ClaudeDo.Ui;
+using ClaudeDo.Ui.Services;
+using ClaudeDo.Ui.ViewModels;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+
+namespace ClaudeDo.App;
+
+sealed class Program
+{
+ [STAThread]
+ public static void Main(string[] args)
+ {
+ var services = BuildServices();
+ App.Services = services;
+
+ // Apply EF Core migrations at startup.
+ using (var scope = services.CreateScope())
+ {
+ scope.ServiceProvider.GetRequiredService().Database.Migrate();
+ }
+
+ try
+ {
+ BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+ }
+ finally
+ {
+ // Dispose the container so WorkerClient.DisposeAsync runs —
+ // cancels the retry loop and closes the SignalR connection cleanly
+ // instead of abandoning it.
+ try { services.DisposeAsync().AsTask().GetAwaiter().GetResult(); }
+ catch { /* best effort on shutdown */ }
+ }
+ }
+
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+#if DEBUG
+ .WithDeveloperTools()
+#endif
+ .WithInterFont()
+ .LogToTrace();
+
+ private static ServiceProvider BuildServices()
+ {
+ var settings = AppSettings.Load();
+ var dbPath = Paths.Expand(settings.DbPath);
+
+ var sc = new ServiceCollection();
+
+ // Infrastructure
+ sc.AddSingleton(settings);
+
+ // EF Core — register both the factory (for singletons) and scoped context.
+ sc.AddDbContextFactory(opt =>
+ opt.UseSqlite($"Data Source={dbPath}"));
+ sc.AddScoped(sp =>
+ sp.GetRequiredService>().CreateDbContext());
+
+ // Repositories — scoped.
+ sc.AddScoped();
+ sc.AddScoped();
+ sc.AddScoped();
+ sc.AddScoped();
+ sc.AddScoped();
+
+ // Services
+ sc.AddSingleton();
+ sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService().SignalRUrl));
+
+ // ViewModels — singletons that use IDbContextFactory for on-demand DB access.
+ sc.AddTransient();
+ sc.AddTransient();
+ sc.AddSingleton();
+ sc.AddSingleton>(sp =>
+ sp.GetRequiredService>());
+
+ // ViewModel factory registrations — these need to be updated in Task 13
+ // to take IDbContextFactory instead of repositories directly.
+ // For now, leave them unchanged — they won't compile until Task 13.
+
+ return sc.BuildServiceProvider();
+ }
+}
+```
+
+Note: This file will NOT compile until Task 13 updates the ViewModels. That is expected.
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/ClaudeDo.App/Program.cs
+git commit -m "refactor(app): switch DI from raw ADO.NET to EF Core"
+```
+
+---
+
+### Task 13: Update ViewModel and Worker Service Consumers
+
+All singleton ViewModels and Worker services currently take repository instances directly. Since repositories are now scoped, these consumers must take `IDbContextFactory` and create a fresh context + repositories per operation.
+
+**Files:**
+- Modify: `src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs`
+- Modify: `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs`
+- Modify: `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs`
+- Modify: `src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs`
+- Modify: `src/ClaudeDo.Worker/Services/QueueService.cs`
+- Modify: `src/ClaudeDo.Worker/Services/StaleTaskRecovery.cs`
+- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs`
+- Modify: `src/ClaudeDo.Worker/Runner/WorktreeManager.cs`
+
+- [ ] **Step 1: Update each consumer**
+
+For each consumer, apply this pattern:
+
+**Before (example — QueueService):**
+```csharp
+private readonly TaskRepository _taskRepo;
+public QueueService(TaskRepository taskRepo, ...) { _taskRepo = taskRepo; ... }
+
+// In method:
+var task = await _taskRepo.GetNextQueuedAgentTaskAsync(now, ct);
+```
+
+**After:**
+```csharp
+private readonly IDbContextFactory _dbFactory;
+public QueueService(IDbContextFactory dbFactory, ...) { _dbFactory = dbFactory; ... }
+
+// In method:
+using var context = _dbFactory.CreateDbContext();
+var taskRepo = new TaskRepository(context);
+var task = await taskRepo.GetNextQueuedAgentTaskAsync(now, ct);
+```
+
+Apply this to every file listed above. Each method that touches the DB creates a context and the repositories it needs. The context is disposed at method end.
+
+**Consumer → repositories used:**
+
+| Consumer | Repositories |
+|---|---|
+| `MainWindowViewModel` | `ListRepository` |
+| `TaskDetailViewModel` | `TaskRepository`, `WorktreeRepository`, `ListRepository`, `TagRepository`, `SubtaskRepository` |
+| `TaskListViewModel` | `TaskRepository`, `TagRepository`, `ListRepository` |
+| `TaskEditorViewModel` | `SubtaskRepository` |
+| `QueueService` | `TaskRepository` |
+| `StaleTaskRecovery` | `TaskRepository` |
+| `TaskRunner` | `TaskRepository`, `TaskRunRepository`, `ListRepository`, `WorktreeRepository`, `SubtaskRepository` |
+| `WorktreeManager` | `WorktreeRepository` |
+
+For each file:
+1. Replace repository fields with a single `IDbContextFactory _dbFactory` field.
+2. Update constructor to take `IDbContextFactory`.
+3. In each method that uses repos: `using var context = _dbFactory.CreateDbContext();` then instantiate needed repos.
+
+- [ ] **Step 2: Update App Program.cs ViewModel registrations**
+
+Now that ViewModels take `IDbContextFactory` instead of repos, simplify the registrations. The long factory lambdas that manually resolved repos can be replaced with simpler registrations since ViewModels now only need `IDbContextFactory` and non-repo services:
+
+```csharp
+ // ViewModels
+ sc.AddTransient();
+ sc.AddTransient();
+ sc.AddSingleton();
+ sc.AddSingleton();
+ sc.AddSingleton();
+ sc.AddSingleton();
+```
+
+If the constructors still take non-repo dependencies (GitService, WorkerClient, StatusBarViewModel, etc.), keep those in the constructor and let DI resolve them naturally.
+
+- [ ] **Step 3: Update Worker Program.cs singleton services**
+
+Worker singleton services (QueueService, TaskRunner, WorktreeManager) now take `IDbContextFactory` instead of repos. Add factory registration and update service registrations:
+
+Add to Worker Program.cs after `AddDbContext`:
+```csharp
+builder.Services.AddDbContextFactory(opt =>
+ opt.UseSqlite($"Data Source={cfg.DbPath}"));
+```
+
+Singleton services will resolve `IDbContextFactory` from DI and create scoped contexts per operation.
+
+- [ ] **Step 4: Verify the solution builds**
+
+Run: `dotnet build ClaudeDo.slnx`
+Expected: Build succeeds with zero errors.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/ClaudeDo.Ui/ViewModels/ src/ClaudeDo.Worker/Services/ src/ClaudeDo.Worker/Runner/ src/ClaudeDo.App/Program.cs src/ClaudeDo.Worker/Program.cs
+git commit -m "refactor(ui,worker): switch consumers from direct repos to IDbContextFactory"
+```
+
+---
+
+### Task 14: Generate Initial EF Core Migration
+
+**Files:**
+- Create: `src/ClaudeDo.Data/Migrations/` (generated)
+
+- [ ] **Step 1: Install EF Core tools if needed**
+
+Run: `dotnet tool list -g | grep dotnet-ef || dotnet tool install --global dotnet-ef --version 8.0.11`
+
+- [ ] **Step 2: Generate the initial migration**
+
+Run from the repo root:
+
+```bash
+dotnet ef migrations add InitialCreate --project src/ClaudeDo.Data --startup-project src/ClaudeDo.Worker
+```
+
+Expected: Creates `src/ClaudeDo.Data/Migrations/` with `*_InitialCreate.cs` and `ClaudeDoDbContextModelSnapshot.cs`.
+
+- [ ] **Step 3: Add existing-database compatibility shim**
+
+Edit the generated `*_InitialCreate.cs` `Up` method. Wrap all `CreateTable` calls in a check for existing tables. Add this at the very start of `Up`:
+
+```csharp
+protected override void Up(MigrationBuilder migrationBuilder)
+{
+ // If upgrading from the old schema.sql-based setup, tables already exist.
+ // Check for the 'lists' table as a sentinel — if it exists, skip DDL and
+ // just ensure the migration is recorded.
+ migrationBuilder.Sql("""
+ CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
+ "MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
+ "ProductVersion" TEXT NOT NULL
+ );
+ """);
+
+ // The rest of the migration uses IF NOT EXISTS for safety.
+ // ... (keep the generated code but add IF NOT EXISTS to each CreateTable)
+}
+```
+
+Alternatively, since SQLite `CREATE TABLE IF NOT EXISTS` is supported, prefix each generated `CREATE TABLE` statement. The simplest approach: in `Up`, replace each `migrationBuilder.CreateTable(...)` with equivalent `migrationBuilder.Sql("CREATE TABLE IF NOT EXISTS ...")` calls. This is a one-time adaptation.
+
+- [ ] **Step 4: Verify migration applies to a fresh database**
+
+```bash
+rm -f /tmp/test_migration.db
+dotnet ef database update --project src/ClaudeDo.Data --startup-project src/ClaudeDo.Worker -- --DbPath /tmp/test_migration.db
+```
+
+Or simply run the Worker pointed at a new DB path and check it starts.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/ClaudeDo.Data/Migrations/
+git commit -m "feat(data): add InitialCreate EF Core migration with existing-DB compat shim"
+```
+
+---
+
+### Task 15: Update Test Infrastructure
+
+**Files:**
+- Modify: `tests/ClaudeDo.Worker.Tests/Infrastructure/DbFixture.cs`
+- Modify: `tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs` (and any other test files that use DbFixture)
+
+- [ ] **Step 1: Rewrite DbFixture to use EF Core**
+
+```csharp
+using ClaudeDo.Data;
+using Microsoft.EntityFrameworkCore;
+
+namespace ClaudeDo.Worker.Tests.Infrastructure;
+
+public sealed class DbFixture : IDisposable
+{
+ public string DbPath { get; }
+ public DbContextOptions Options { get; }
+
+ public DbFixture()
+ {
+ DbPath = Path.Combine(Path.GetTempPath(), $"claudedo_test_{Guid.NewGuid():N}.db");
+ Options = new DbContextOptionsBuilder()
+ .UseSqlite($"Data Source={DbPath}")
+ .Options;
+
+ using var context = CreateContext();
+ context.Database.Migrate();
+ }
+
+ public ClaudeDoDbContext CreateContext() => new(Options);
+
+ public void Dispose()
+ {
+ try { File.Delete(DbPath); } catch { /* best effort */ }
+ try { File.Delete(DbPath + "-wal"); } catch { }
+ try { File.Delete(DbPath + "-shm"); } catch { }
+ }
+}
+```
+
+- [ ] **Step 2: Update WorktreeManagerTests and other test files**
+
+Replace `new ListRepository(db.Factory)` with `new ListRepository(db.CreateContext())` etc. For example in WorktreeManagerTests `CreateManagerAsync`:
+
+```csharp
+private async Task<(WorktreeManager mgr, WorktreeRepository wtRepo)> CreateManagerAsync(
+ TaskEntity task, ListEntity list, string strategy = "sibling", string? centralRoot = null)
+{
+ var db = new DbFixture();
+ _dbFixtures.Add(db);
+
+ var context = db.CreateContext();
+ var listRepo = new ListRepository(context);
+ var taskRepo = new TaskRepository(context);
+ var wtRepo = new WorktreeRepository(context);
+ // ... seed and create manager as before, but using context-based repos
+```
+
+Apply the same pattern to all test files that use `DbFixture.Factory` to construct repositories.
+
+- [ ] **Step 3: Run tests**
+
+Run: `dotnet test tests/ClaudeDo.Worker.Tests`
+Expected: All existing tests pass.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add tests/ClaudeDo.Worker.Tests/
+git commit -m "refactor(tests): switch test infrastructure to EF Core DbContext"
+```
+
+---
+
+### Task 16: Full Build and Test Verification
+
+- [ ] **Step 1: Clean build**
+
+Run: `dotnet build ClaudeDo.slnx --no-incremental`
+Expected: Build succeeds with zero errors.
+
+- [ ] **Step 2: Run all tests**
+
+Run: `dotnet test ClaudeDo.slnx`
+Expected: All tests pass.
+
+- [ ] **Step 3: Smoke test — start Worker**
+
+Run the Worker and verify it starts, creates/migrates the DB, and logs the listening message.
+
+- [ ] **Step 4: Smoke test — start App**
+
+Run the App and verify it starts, connects to Worker (if running), and the UI loads.
+
+- [ ] **Step 5: Verify no references to old types remain**
+
+```bash
+grep -rn "SqliteConnectionFactory\|SchemaInitializer\|Microsoft\.Data\.Sqlite\|using Microsoft\.Data\.Sqlite" src/ tests/ --include="*.cs" --include="*.csproj"
+```
+
+Expected: Zero matches.
+
+- [ ] **Step 6: Final commit if any cleanup was needed**
+
+```bash
+git add -A
+git commit -m "chore(data): final cleanup after EF Core migration"
+```
+
+---
+
+### Task 17: Update Documentation
+
+**Files:**
+- Modify: `CLAUDE.md`
+- Modify: `src/ClaudeDo.Data/CLAUDE.md`
+
+- [ ] **Step 1: Update project CLAUDE.md**
+
+Update the Tech Stack, Conventions, and Key Paths sections to reflect EF Core:
+- Replace "SQLite (WAL mode) via Microsoft.Data.Sqlite — raw ADO.NET, no ORM" with "SQLite (WAL mode) via Entity Framework Core + Microsoft.EntityFrameworkCore.Sqlite"
+- Remove references to `schema/schema.sql`, `SqliteConnectionFactory`, `SchemaInitializer`
+- Update repository description to mention LINQ instead of raw SQL
+- Add note about `IDbContextFactory` pattern for singleton consumers
+
+- [ ] **Step 2: Update ClaudeDo.Data/CLAUDE.md**
+
+Rewrite the Infrastructure and Repositories sections:
+- Infrastructure: `ClaudeDoDbContext` replaces `SqliteConnectionFactory` + `SchemaInitializer`
+- Repositories: "All repositories use EF Core LINQ. Exception: `TaskRepository.GetNextQueuedAgentTaskAsync` uses `FromSqlRaw` for atomic queue claim."
+- Remove Conventions about `ToDb()`/`FromDb()` and `DBNull.Value`
+- Add Conventions about `IEntityTypeConfiguration` in `Configuration/` folder
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add CLAUDE.md src/ClaudeDo.Data/CLAUDE.md
+git commit -m "docs: update CLAUDE.md files for EF Core migration"
+```