# 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
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

</Project>
```

- [ ] **Step 2: Update Worker.Tests.csproj**

Replace `Microsoft.Data.Sqlite` with EF Core:

```xml
  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="6.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
    <PackageReference Include="xunit" Version="2.5.3" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
  </ItemGroup>
```

- [ ] **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<TagEntity> Tags { get; set; } = new List<TagEntity>();
    public ICollection<TaskRunEntity> Runs { get; set; } = new List<TaskRunEntity>();
    public ICollection<SubtaskEntity> Subtasks { get; set; } = new List<SubtaskEntity>();
}
```

- [ ] **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<TaskEntity> Tasks { get; set; } = new List<TaskEntity>();
    public ICollection<TagEntity> Tags { get; set; } = new List<TagEntity>();
}
```

- [ ] **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<ListEntity> Lists { get; set; } = new List<ListEntity>();
    public ICollection<TaskEntity> Tasks { get; set; } = new List<TaskEntity>();
}
```

- [ ] **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<ClaudeDoDbContext> options) : base(options) { }

    public DbSet<TaskEntity> Tasks => Set<TaskEntity>();
    public DbSet<ListEntity> Lists => Set<ListEntity>();
    public DbSet<TagEntity> Tags => Set<TagEntity>();
    public DbSet<ListConfigEntity> ListConfigs => Set<ListConfigEntity>();
    public DbSet<WorktreeEntity> Worktrees => Set<WorktreeEntity>();
    public DbSet<TaskRunEntity> TaskRuns => Set<TaskRunEntity>();
    public DbSet<SubtaskEntity> Subtasks => Set<SubtaskEntity>();

    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<TaskEntity>
{
    public void Configure(EntityTypeBuilder<TaskEntity> 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<WorktreeEntity>(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<ListEntity>
{
    public void Configure(EntityTypeBuilder<ListEntity> 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<ListConfigEntity>(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<TagEntity>
{
    public void Configure(EntityTypeBuilder<TagEntity> 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<ListConfigEntity>
{
    public void Configure(EntityTypeBuilder<ListConfigEntity> 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<WorktreeEntity>
{
    public void Configure(EntityTypeBuilder<WorktreeEntity> 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<TaskRunEntity>
{
    public void Configure(EntityTypeBuilder<TaskRunEntity> 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<SubtaskEntity>
{
    public void Configure(EntityTypeBuilder<SubtaskEntity> 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<List<TagEntity>> GetAllAsync(CancellationToken ct = default)
    {
        return await _context.Tags.OrderBy(t => t.Id).ToListAsync(ct);
    }

    public async Task<long> 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<List<SubtaskEntity>> 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<WorktreeEntity?> 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<ListEntity?> GetByIdAsync(string listId, CancellationToken ct = default)
    {
        return await _context.Lists.FirstOrDefaultAsync(l => l.Id == listId, ct);
    }

    public async Task<List<ListEntity>> GetAllAsync(CancellationToken ct = default)
    {
        return await _context.Lists.OrderBy(l => l.CreatedAt).ToListAsync(ct);
    }

    // Tag management via junction table

    public async Task<List<TagEntity>> 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<ListConfigEntity?> 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<TaskRunEntity?> GetByIdAsync(string id, CancellationToken ct = default)
    {
        return await _context.TaskRuns.FirstOrDefaultAsync(r => r.Id == id, ct);
    }

    public async Task<List<TaskRunEntity>> GetByTaskIdAsync(string taskId, CancellationToken ct = default)
    {
        return await _context.TaskRuns
            .Where(r => r.TaskId == taskId)
            .OrderBy(r => r.RunNumber)
            .ToListAsync(ct);
    }

    public async Task<TaskRunEntity?> 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<TaskEntity?> GetByIdAsync(string taskId, CancellationToken ct = default)
    {
        return await _context.Tasks.FirstOrDefaultAsync(t => t.Id == taskId, ct);
    }

    public async Task<List<TaskEntity>> 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<List<TagEntity>> GetTagsAsync(string taskId, CancellationToken ct = default)
    {
        return await _context.Tasks
            .Where(t => t.Id == taskId)
            .SelectMany(t => t.Tags)
            .ToListAsync(ct);
    }

    public async Task<List<TagEntity>> 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<TaskEntity?> 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<ClaudeDoDbContext>(opt =>
    opt.UseSqlite($"Data Source={cfg.DbPath}"));

// Repositories — scoped (they depend on the scoped DbContext).
builder.Services.AddScoped<TagRepository>();
builder.Services.AddScoped<ListRepository>();
builder.Services.AddScoped<TaskRepository>();
builder.Services.AddScoped<SubtaskRepository>();
builder.Services.AddScoped<WorktreeRepository>();
builder.Services.AddScoped<TaskRunRepository>();

builder.Services.AddHostedService<StaleTaskRecovery>();
builder.Services.AddSignalR();

// Runner stack.
builder.Services.AddSingleton<IClaudeProcess, ClaudeProcess>();
builder.Services.AddSingleton<HubBroadcaster>();
builder.Services.AddSingleton<GitService>();
builder.Services.AddSingleton<WorktreeManager>();
builder.Services.AddSingleton<ClaudeArgsBuilder>();
builder.Services.AddSingleton(new AgentFileService(Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
    ".todo-app", "agents")));

// QueueService: singleton + hosted service (same instance).
builder.Services.AddSingleton<QueueService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<QueueService>());

// 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<ClaudeDoDbContext>();
    db.Database.Migrate();
}

app.MapHub<WorkerHub>("/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<ClaudeDoDbContext>` 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<ClaudeDoDbContext>().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<App>()
            .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<ClaudeDoDbContext>(opt =>
            opt.UseSqlite($"Data Source={dbPath}"));
        sc.AddScoped<ClaudeDoDbContext>(sp =>
            sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());

        // Repositories — scoped.
        sc.AddScoped<ListRepository>();
        sc.AddScoped<TaskRepository>();
        sc.AddScoped<SubtaskRepository>();
        sc.AddScoped<TagRepository>();
        sc.AddScoped<WorktreeRepository>();

        // Services
        sc.AddSingleton<GitService>();
        sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService<AppSettings>().SignalRUrl));

        // ViewModels — singletons that use IDbContextFactory for on-demand DB access.
        sc.AddTransient<ListEditorViewModel>();
        sc.AddTransient<TaskEditorViewModel>();
        sc.AddSingleton<StatusBarViewModel>();
        sc.AddSingleton<IDbContextFactory<ClaudeDoDbContext>>(sp =>
            sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>());

        // 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<ClaudeDoDbContext>` 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<ClaudeDoDbContext> _dbFactory;
public QueueService(IDbContextFactory<ClaudeDoDbContext> 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<ClaudeDoDbContext> _dbFactory` field.
2. Update constructor to take `IDbContextFactory<ClaudeDoDbContext>`.
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<ListEditorViewModel>();
        sc.AddTransient<TaskEditorViewModel>();
        sc.AddSingleton<StatusBarViewModel>();
        sc.AddSingleton<TaskDetailViewModel>();
        sc.AddSingleton<TaskListViewModel>();
        sc.AddSingleton<MainWindowViewModel>();
```

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<ClaudeDoDbContext>(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<ClaudeDoDbContext> Options { get; }

    public DbFixture()
    {
        DbPath = Path.Combine(Path.GetTempPath(), $"claudedo_test_{Guid.NewGuid():N}.db");
        Options = new DbContextOptionsBuilder<ClaudeDoDbContext>()
            .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<T>` 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"
```
