# Planning Sessions — Plan A: Foundation 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:** Add the database, enum, repository, and auto-status infrastructure that enables planning sessions — no UI, no terminal, no MCP. Everything on top of this plan depends on the schema and repo methods landing cleanly.

**Architecture:** Extend `TaskEntity` with four new columns (`ParentTaskId`, `PlanningSessionId`, `PlanningSessionToken`, `PlanningFinalizedAt`), add three enum values (`Planning`, `Planned`, `Draft`), configure a self-referential FK with `DeleteBehavior.Restrict`, generate an EF migration, add seven new repository methods for the planning lifecycle, and wire a parent-auto-completion hook into the TaskRunner so that when a child reaches a terminal state the parent's status updates accordingly.

**Tech Stack:** .NET 8, EF Core (SQLite provider), xUnit. Uses existing `DbFixture` (real SQLite per test) and `TaskRepository` sealed-class pattern.

**Spec reference:** `docs/superpowers/specs/2026-04-23-planning-sessions-design.md` sections 2, 3, 7.1, 7.2, 8.1.

---

## File Structure

**Modified:**
- `src/ClaudeDo.Data/Models/TaskEntity.cs` — add four columns + navigation properties, extend `TaskStatus` enum.
- `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs` — map new columns, configure self-ref FK, extend status converter.
- `src/ClaudeDo.Data/Repositories/TaskRepository.cs` — add seven planning methods.
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — call `TryCompleteParentAsync` after `MarkDoneAsync`/`MarkFailedAsync`.

**Created:**
- `src/ClaudeDo.Data/Migrations/<timestamp>_AddPlanningSupport.cs` — EF migration.
- `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs` — new test class for planning methods.
- `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryParentCompletionTests.cs` — tests for auto-status hook.

**No interface extraction:** existing code uses `TaskRepository` sealed class directly (no `ITaskRepository`); we follow that pattern.

**Marker for parallel plans B and C:** the file `src/ClaudeDo.Data/Migrations/<timestamp>_AddPlanningSupport.cs` existing on `main` is the signal that Plan A is merged. Plans B and C should poll for it before starting.

---

## Task 1: Extend `TaskStatus` enum

**Files:**
- Modify: `src/ClaudeDo.Data/Models/TaskEntity.cs`
- Modify: `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs`

- [ ] **Step 1: Add enum values**

In `src/ClaudeDo.Data/Models/TaskEntity.cs`, extend the enum:

```csharp
public enum TaskStatus
{
    Manual,
    Queued,
    Running,
    Done,
    Failed,
    Planning,
    Planned,
    Draft,
}
```

- [ ] **Step 2: Extend status converter**

In `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs`, extend both converter methods to handle the three new values:

```csharp
private static string StatusToString(TaskStatus v)
    => v == TaskStatus.Manual ? "manual"
     : v == TaskStatus.Queued ? "queued"
     : v == TaskStatus.Running ? "running"
     : v == TaskStatus.Done ? "done"
     : v == TaskStatus.Failed ? "failed"
     : v == TaskStatus.Planning ? "planning"
     : v == TaskStatus.Planned ? "planned"
     : v == TaskStatus.Draft ? "draft"
     : throw new ArgumentOutOfRangeException(nameof(v));

private static TaskStatus StatusFromString(string v)
    => v == "manual" ? TaskStatus.Manual
     : v == "queued" ? TaskStatus.Queued
     : v == "running" ? TaskStatus.Running
     : v == "done" ? TaskStatus.Done
     : v == "failed" ? TaskStatus.Failed
     : v == "planning" ? TaskStatus.Planning
     : v == "planned" ? TaskStatus.Planned
     : v == "draft" ? TaskStatus.Draft
     : throw new ArgumentOutOfRangeException(nameof(v));
```

- [ ] **Step 3: Build**

Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj`
Expected: build succeeds.

- [ ] **Step 4: Commit**

```bash
git add src/ClaudeDo.Data/Models/TaskEntity.cs src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs
git commit -m "feat(data): add Planning, Planned, Draft task statuses"
```

---

## Task 2: Extend `TaskEntity` with planning columns and navigations

**Files:**
- Modify: `src/ClaudeDo.Data/Models/TaskEntity.cs`

- [ ] **Step 1: Add four properties + navigations**

In `TaskEntity` class, add (placement: after `SortOrder`, before navigation section):

```csharp
public int SortOrder { get; set; }

public string? ParentTaskId { get; set; }
public string? PlanningSessionId { get; set; }
public string? PlanningSessionToken { get; set; }
public DateTime? PlanningFinalizedAt { 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>();

public TaskEntity? Parent { get; set; }
public ICollection<TaskEntity> Children { get; set; } = new List<TaskEntity>();
```

- [ ] **Step 2: Build**

Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj`
Expected: build succeeds.

- [ ] **Step 3: Commit**

```bash
git add src/ClaudeDo.Data/Models/TaskEntity.cs
git commit -m "feat(data): add planning columns and self-ref navigations to TaskEntity"
```

---

## Task 3: Configure self-ref FK and new columns in `TaskEntityConfiguration`

**Files:**
- Modify: `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs`

- [ ] **Step 1: Map the four new columns + configure self-ref FK**

After the existing `SortOrder` property mapping (currently ends with `.HasDefaultValue(0);`), insert:

```csharp
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);

builder.Property(t => t.ParentTaskId).HasColumnName("parent_task_id");
builder.Property(t => t.PlanningSessionId).HasColumnName("planning_session_id");
builder.Property(t => t.PlanningSessionToken).HasColumnName("planning_session_token");
builder.Property(t => t.PlanningFinalizedAt).HasColumnName("planning_finalized_at");

builder.HasOne(t => t.Parent)
    .WithMany(t => t.Children)
    .HasForeignKey(t => t.ParentTaskId)
    .OnDelete(DeleteBehavior.Restrict);

builder.HasOne(t => t.List)
```

(The existing `builder.HasOne(t => t.List)` block immediately follows; do not duplicate it — just insert the new block above it.)

- [ ] **Step 2: Add index on `ParentTaskId`**

At the end of `Configure`, after the existing `HasIndex` calls:

```csharp
builder.HasIndex(t => t.ParentTaskId).HasDatabaseName("idx_tasks_parent_task_id");
```

- [ ] **Step 3: Build**

Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj`
Expected: build succeeds.

- [ ] **Step 4: Commit**

```bash
git add src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs
git commit -m "feat(data): configure planning columns and self-ref FK with Restrict"
```

---

## Task 4: Generate EF migration

**Files:**
- Create: `src/ClaudeDo.Data/Migrations/<timestamp>_AddPlanningSupport.cs` (auto-generated, then reviewed)

- [ ] **Step 1: Scaffold the migration**

Run from project root:

```bash
dotnet ef migrations add AddPlanningSupport \
  --project src/ClaudeDo.Data \
  --startup-project src/ClaudeDo.Worker
```

Expected: three new files under `src/ClaudeDo.Data/Migrations/` (the migration, its designer file, and an updated model snapshot).

- [ ] **Step 2: Review the generated migration**

Open the new `<timestamp>_AddPlanningSupport.cs` file. Verify the `Up` method contains:
- Four `AddColumn<string>` / `AddColumn<DateTime>` calls for `parent_task_id` (nullable string), `planning_session_id` (nullable string), `planning_session_token` (nullable string), `planning_finalized_at` (nullable DateTime).
- A `CreateIndex` for `idx_tasks_parent_task_id` on `parent_task_id`.
- An `AddForeignKey` call with `onDelete: ReferentialAction.Restrict` linking `parent_task_id` → `tasks.id`.

No data migration needed (all new columns nullable).

- [ ] **Step 3: Apply to a scratch DB to verify**

Run:

```bash
dotnet ef database update \
  --project src/ClaudeDo.Data \
  --startup-project src/ClaudeDo.Worker \
  --connection "Data Source=./scratch_migration_test.db"
```

Expected: no errors. Then delete the scratch DB file:

```bash
rm -f ./scratch_migration_test.db ./scratch_migration_test.db-wal ./scratch_migration_test.db-shm
```

- [ ] **Step 4: Commit**

```bash
git add src/ClaudeDo.Data/Migrations/
git commit -m "feat(data): migration AddPlanningSupport"
```

---

## Task 5: Repository method `GetChildrenAsync`

**Files:**
- Test: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs` (create)
- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs`

- [ ] **Step 1: Create the test class skeleton**

Create `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs`:

```csharp
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Tests.Infrastructure;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Repositories;

public sealed class TaskRepositoryPlanningTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly ClaudeDoDbContext _ctx;
    private readonly TaskRepository _tasks;
    private readonly ListRepository _lists;
    private readonly TagRepository _tags;

    public TaskRepositoryPlanningTests()
    {
        _ctx = _db.CreateContext();
        _tasks = new TaskRepository(_ctx);
        _lists = new ListRepository(_ctx);
        _tags = new TagRepository(_ctx);
    }

    public void Dispose()
    {
        _ctx.Dispose();
        _db.Dispose();
    }

    private async Task<string> CreateListAsync(string? id = null)
    {
        var listId = id ?? Guid.NewGuid().ToString();
        await _lists.AddAsync(new ListEntity
        {
            Id = listId,
            Name = "Test List",
            CreatedAt = DateTime.UtcNow,
        });
        return listId;
    }

    private TaskEntity MakeTask(string listId, TaskStatus status = TaskStatus.Manual, string? parentId = null) => new()
    {
        Id = Guid.NewGuid().ToString(),
        ListId = listId,
        Title = "t",
        Status = status,
        CreatedAt = DateTime.UtcNow,
        CommitType = "feat",
        ParentTaskId = parentId,
    };
}
```

- [ ] **Step 2: Write the failing test for `GetChildrenAsync`**

Add inside the class:

```csharp
[Fact]
public async Task GetChildrenAsync_ReturnsOnlyDirectChildren_Sorted()
{
    var listId = await CreateListAsync();
    var parent = MakeTask(listId, TaskStatus.Planning);
    parent.Title = "parent";
    await _tasks.AddAsync(parent);

    var childA = MakeTask(listId, TaskStatus.Draft, parentId: parent.Id);
    childA.Title = "a"; childA.SortOrder = 1;
    await _tasks.AddAsync(childA);

    var childB = MakeTask(listId, TaskStatus.Draft, parentId: parent.Id);
    childB.Title = "b"; childB.SortOrder = 0;
    await _tasks.AddAsync(childB);

    var unrelated = MakeTask(listId, TaskStatus.Manual);
    await _tasks.AddAsync(unrelated);

    var children = await _tasks.GetChildrenAsync(parent.Id);

    Assert.Equal(2, children.Count);
    Assert.Equal("b", children[0].Title);
    Assert.Equal("a", children[1].Title);
}
```

- [ ] **Step 3: Run the test; verify it fails**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryPlanningTests.GetChildrenAsync"`
Expected: FAIL — `TaskRepository` has no method `GetChildrenAsync`.

- [ ] **Step 4: Implement the method**

In `src/ClaudeDo.Data/Repositories/TaskRepository.cs`, add a new region at the end of the class (before the final `}`):

```csharp
    #region Planning

    public async Task<List<TaskEntity>> GetChildrenAsync(string parentId, CancellationToken ct = default)
    {
        return await _context.Tasks
            .AsNoTracking()
            .Where(t => t.ParentTaskId == parentId)
            .OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
            .ToListAsync(ct);
    }

    #endregion
```

- [ ] **Step 5: Run the test; verify it passes**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryPlanningTests.GetChildrenAsync"`
Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs src/ClaudeDo.Data/Repositories/TaskRepository.cs
git commit -m "feat(data): TaskRepository.GetChildrenAsync"
```

---

## Task 6: Repository method `CreateChildAsync`

**Files:**
- Modify: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs`
- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs`

- [ ] **Step 1: Write the failing test**

Append to the planning test class:

```csharp
[Fact]
public async Task CreateChildAsync_CreatesDraftUnderParent()
{
    var listId = await CreateListAsync();
    var parent = MakeTask(listId, TaskStatus.Planning);
    await _tasks.AddAsync(parent);

    var child = await _tasks.CreateChildAsync(
        parent.Id,
        title: "child title",
        description: "child desc",
        tagNames: new[] { "agent" },
        commitType: "feat");

    Assert.Equal(TaskStatus.Draft, child.Status);
    Assert.Equal(parent.Id, child.ParentTaskId);
    Assert.Equal(listId, child.ListId);
    Assert.Equal("child title", child.Title);
    Assert.Equal("child desc", child.Description);
    Assert.Equal("feat", child.CommitType);

    var loaded = await _tasks.GetByIdAsync(child.Id);
    Assert.NotNull(loaded);
    Assert.Equal(TaskStatus.Draft, loaded!.Status);

    var tags = await _tasks.GetTagsAsync(child.Id);
    Assert.Contains(tags, t => t.Name == "agent");
}

[Fact]
public async Task CreateChildAsync_ThrowsIfParentNotFound()
{
    var listId = await CreateListAsync();
    _ = listId; // just to create the DB

    await Assert.ThrowsAsync<InvalidOperationException>(() =>
        _tasks.CreateChildAsync("nonexistent-parent-id", "t", null, null, null));
}
```

- [ ] **Step 2: Run; verify fail**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryPlanningTests.CreateChildAsync"`
Expected: FAIL (method not defined).

- [ ] **Step 3: Implement**

In the `#region Planning` section of `TaskRepository.cs`, add:

```csharp
public async Task<TaskEntity> CreateChildAsync(
    string parentId,
    string title,
    string? description,
    IReadOnlyList<string>? tagNames,
    string? commitType,
    CancellationToken ct = default)
{
    var parent = await _context.Tasks.FirstOrDefaultAsync(t => t.Id == parentId, ct);
    if (parent is null)
        throw new InvalidOperationException($"Parent task {parentId} not found.");

    var maxSort = await _context.Tasks
        .Where(t => t.ListId == parent.ListId)
        .Select(t => (int?)t.SortOrder)
        .MaxAsync(ct);

    var child = new TaskEntity
    {
        Id = Guid.NewGuid().ToString(),
        ListId = parent.ListId,
        Title = title,
        Description = description,
        Status = TaskStatus.Draft,
        CreatedAt = DateTime.UtcNow,
        CommitType = string.IsNullOrEmpty(commitType) ? parent.CommitType : commitType,
        ParentTaskId = parentId,
        SortOrder = (maxSort ?? -1) + 1,
    };
    _context.Tasks.Add(child);

    if (tagNames is not null && tagNames.Count > 0)
    {
        foreach (var tagName in tagNames.Distinct(StringComparer.OrdinalIgnoreCase))
        {
            var tag = await _context.Tags.FirstOrDefaultAsync(t => t.Name == tagName, ct);
            if (tag is null)
            {
                tag = new TagEntity { Name = tagName };
                _context.Tags.Add(tag);
                await _context.SaveChangesAsync(ct);
            }
            child.Tags.Add(tag);
        }
    }

    await _context.SaveChangesAsync(ct);
    return child;
}
```

- [ ] **Step 4: Run; verify pass**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryPlanningTests.CreateChildAsync"`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs src/ClaudeDo.Data/Repositories/TaskRepository.cs
git commit -m "feat(data): TaskRepository.CreateChildAsync"
```

---

## Task 7: Repository method `SetPlanningStartedAsync`

**Files:**
- Modify: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs`
- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs`

- [ ] **Step 1: Write the failing test**

Append:

```csharp
[Fact]
public async Task SetPlanningStartedAsync_ManualTask_TransitionsToPlanning()
{
    var listId = await CreateListAsync();
    var task = MakeTask(listId, TaskStatus.Manual);
    await _tasks.AddAsync(task);

    var result = await _tasks.SetPlanningStartedAsync(task.Id, "tok-abc");

    Assert.NotNull(result);
    Assert.Equal(TaskStatus.Planning, result!.Status);
    Assert.Equal("tok-abc", result.PlanningSessionToken);

    var loaded = await _tasks.GetByIdAsync(task.Id);
    Assert.Equal(TaskStatus.Planning, loaded!.Status);
    Assert.Equal("tok-abc", loaded.PlanningSessionToken);
}

[Fact]
public async Task SetPlanningStartedAsync_NonManualTask_ReturnsNull()
{
    var listId = await CreateListAsync();
    var task = MakeTask(listId, TaskStatus.Queued);
    await _tasks.AddAsync(task);

    var result = await _tasks.SetPlanningStartedAsync(task.Id, "tok-xyz");

    Assert.Null(result);
    var loaded = await _tasks.GetByIdAsync(task.Id);
    Assert.Equal(TaskStatus.Queued, loaded!.Status);
    Assert.Null(loaded.PlanningSessionToken);
}
```

- [ ] **Step 2: Run; verify fail**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryPlanningTests.SetPlanningStarted"`
Expected: FAIL.

- [ ] **Step 3: Implement**

In `#region Planning`:

```csharp
public async Task<TaskEntity?> SetPlanningStartedAsync(
    string taskId,
    string sessionToken,
    CancellationToken ct = default)
{
    var affected = await _context.Tasks
        .Where(t => t.Id == taskId && t.Status == TaskStatus.Manual)
        .ExecuteUpdateAsync(s => s
            .SetProperty(t => t.Status, TaskStatus.Planning)
            .SetProperty(t => t.PlanningSessionToken, sessionToken), ct);

    if (affected == 0) return null;
    return await _context.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId, ct);
}
```

- [ ] **Step 4: Run; verify pass**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryPlanningTests.SetPlanningStarted"`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs src/ClaudeDo.Data/Repositories/TaskRepository.cs
git commit -m "feat(data): TaskRepository.SetPlanningStartedAsync"
```

---

## Task 8: Repository method `UpdatePlanningSessionIdAsync`

**Files:**
- Modify: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs`
- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs`

- [ ] **Step 1: Write the failing test**

```csharp
[Fact]
public async Task UpdatePlanningSessionIdAsync_StoresClaudeSessionId()
{
    var listId = await CreateListAsync();
    var task = MakeTask(listId, TaskStatus.Manual);
    await _tasks.AddAsync(task);
    await _tasks.SetPlanningStartedAsync(task.Id, "tok");

    await _tasks.UpdatePlanningSessionIdAsync(task.Id, "claude-session-42");

    var loaded = await _tasks.GetByIdAsync(task.Id);
    Assert.Equal("claude-session-42", loaded!.PlanningSessionId);
}
```

- [ ] **Step 2: Run; verify fail**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryPlanningTests.UpdatePlanningSessionId"`
Expected: FAIL.

- [ ] **Step 3: Implement**

```csharp
public async Task UpdatePlanningSessionIdAsync(
    string parentId,
    string sessionId,
    CancellationToken ct = default)
{
    await _context.Tasks
        .Where(t => t.Id == parentId)
        .ExecuteUpdateAsync(s => s
            .SetProperty(t => t.PlanningSessionId, sessionId), ct);
}
```

- [ ] **Step 4: Run; verify pass**

Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs src/ClaudeDo.Data/Repositories/TaskRepository.cs
git commit -m "feat(data): TaskRepository.UpdatePlanningSessionIdAsync"
```

---

## Task 9: Repository method `FindByPlanningTokenAsync`

**Files:**
- Modify: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs`
- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs`

- [ ] **Step 1: Write the failing test**

```csharp
[Fact]
public async Task FindByPlanningTokenAsync_ReturnsTask_WhenTokenMatches()
{
    var listId = await CreateListAsync();
    var task = MakeTask(listId, TaskStatus.Manual);
    await _tasks.AddAsync(task);
    await _tasks.SetPlanningStartedAsync(task.Id, "unique-token-123");

    var found = await _tasks.FindByPlanningTokenAsync("unique-token-123");

    Assert.NotNull(found);
    Assert.Equal(task.Id, found!.Id);
}

[Fact]
public async Task FindByPlanningTokenAsync_ReturnsNull_WhenTokenUnknown()
{
    var found = await _tasks.FindByPlanningTokenAsync("no-such-token");
    Assert.Null(found);
}
```

- [ ] **Step 2: Run; verify fail**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryPlanningTests.FindByPlanningToken"`
Expected: FAIL.

- [ ] **Step 3: Implement**

```csharp
public async Task<TaskEntity?> FindByPlanningTokenAsync(
    string token,
    CancellationToken ct = default)
{
    if (string.IsNullOrEmpty(token)) return null;
    return await _context.Tasks
        .AsNoTracking()
        .FirstOrDefaultAsync(t => t.PlanningSessionToken == token, ct);
}
```

- [ ] **Step 4: Run; verify pass**

Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs src/ClaudeDo.Data/Repositories/TaskRepository.cs
git commit -m "feat(data): TaskRepository.FindByPlanningTokenAsync"
```

---

## Task 10: Repository method `FinalizePlanningAsync`

**Files:**
- Modify: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs`
- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs`

- [ ] **Step 1: Write the failing test**

```csharp
[Fact]
public async Task FinalizePlanningAsync_TransitionsDraftsAndParent()
{
    var listId = await CreateListAsync();
    var parent = MakeTask(listId, TaskStatus.Manual);
    await _tasks.AddAsync(parent);
    await _tasks.SetPlanningStartedAsync(parent.Id, "tok");

    var c1 = await _tasks.CreateChildAsync(parent.Id, "c1", null, tagNames: new[] { "agent" }, commitType: null);
    var c2 = await _tasks.CreateChildAsync(parent.Id, "c2", null, tagNames: null, commitType: null);

    var count = await _tasks.FinalizePlanningAsync(parent.Id, queueAgentTasks: true);

    Assert.Equal(2, count);

    var c1Loaded = await _tasks.GetByIdAsync(c1.Id);
    var c2Loaded = await _tasks.GetByIdAsync(c2.Id);
    var parentLoaded = await _tasks.GetByIdAsync(parent.Id);

    Assert.Equal(TaskStatus.Queued, c1Loaded!.Status);
    Assert.Equal(TaskStatus.Manual, c2Loaded!.Status);
    Assert.Equal(TaskStatus.Planned, parentLoaded!.Status);
    Assert.NotNull(parentLoaded.PlanningFinalizedAt);
    Assert.Null(parentLoaded.PlanningSessionToken);
}

[Fact]
public async Task FinalizePlanningAsync_QueueAgentTasksFalse_AllToManual()
{
    var listId = await CreateListAsync();
    var parent = MakeTask(listId, TaskStatus.Manual);
    await _tasks.AddAsync(parent);
    await _tasks.SetPlanningStartedAsync(parent.Id, "tok");
    var c = await _tasks.CreateChildAsync(parent.Id, "c", null, tagNames: new[] { "agent" }, commitType: null);

    await _tasks.FinalizePlanningAsync(parent.Id, queueAgentTasks: false);

    var cLoaded = await _tasks.GetByIdAsync(c.Id);
    Assert.Equal(TaskStatus.Manual, cLoaded!.Status);
}

[Fact]
public async Task FinalizePlanningAsync_ParentWithAgentListTag_ChildIsQueued()
{
    var listId = await CreateListAsync();
    var agentTag = await _tags.GetOrCreateAsync("agent");
    // Attach "agent" tag to the list (list-level tags propagate to its tasks).
    await _lists.AddTagAsync(listId, agentTag.Id);

    var parent = MakeTask(listId, TaskStatus.Manual);
    await _tasks.AddAsync(parent);
    await _tasks.SetPlanningStartedAsync(parent.Id, "tok");
    var c = await _tasks.CreateChildAsync(parent.Id, "c", null, tagNames: null, commitType: null);

    await _tasks.FinalizePlanningAsync(parent.Id, queueAgentTasks: true);

    var cLoaded = await _tasks.GetByIdAsync(c.Id);
    Assert.Equal(TaskStatus.Queued, cLoaded!.Status);
}
```

- [ ] **Step 2: Run; verify fail**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryPlanningTests.FinalizePlanning"`
Expected: FAIL.

- [ ] **Step 3: Implement**

```csharp
public async Task<int> FinalizePlanningAsync(
    string parentId,
    bool queueAgentTasks,
    CancellationToken ct = default)
{
    using var tx = await _context.Database.BeginTransactionAsync(ct);

    var parent = await _context.Tasks
        .Include(t => t.List).ThenInclude(l => l.Tags)
        .FirstOrDefaultAsync(t => t.Id == parentId, ct);
    if (parent is null || parent.Status != TaskStatus.Planning)
        throw new InvalidOperationException($"Task {parentId} is not in Planning state.");

    var listHasAgentTag = parent.List.Tags.Any(t => t.Name == "agent");

    var drafts = await _context.Tasks
        .Include(t => t.Tags)
        .Where(t => t.ParentTaskId == parentId && t.Status == TaskStatus.Draft)
        .ToListAsync(ct);

    int count = 0;
    foreach (var draft in drafts)
    {
        var childHasAgentTag = draft.Tags.Any(t => t.Name == "agent");
        var shouldQueue = queueAgentTasks && (childHasAgentTag || listHasAgentTag);
        draft.Status = shouldQueue ? TaskStatus.Queued : TaskStatus.Manual;
        count++;
    }

    parent.Status = TaskStatus.Planned;
    parent.PlanningFinalizedAt = DateTime.UtcNow;
    parent.PlanningSessionToken = null;

    await _context.SaveChangesAsync(ct);
    await tx.CommitAsync(ct);
    return count;
}
```

- [ ] **Step 4: Run; verify pass**

Expected: PASS for all three finalize tests.

- [ ] **Step 5: Commit**

```bash
git add tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs src/ClaudeDo.Data/Repositories/TaskRepository.cs
git commit -m "feat(data): TaskRepository.FinalizePlanningAsync"
```

---

## Task 11: Repository method `DiscardPlanningAsync`

**Files:**
- Modify: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs`
- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs`

- [ ] **Step 1: Write the failing test**

```csharp
[Fact]
public async Task DiscardPlanningAsync_DeletesDraftsAndResetsParent()
{
    var listId = await CreateListAsync();
    var parent = MakeTask(listId, TaskStatus.Manual);
    await _tasks.AddAsync(parent);
    await _tasks.SetPlanningStartedAsync(parent.Id, "tok");
    await _tasks.UpdatePlanningSessionIdAsync(parent.Id, "claude-42");
    var c1 = await _tasks.CreateChildAsync(parent.Id, "c1", null, null, null);
    var c2 = await _tasks.CreateChildAsync(parent.Id, "c2", null, null, null);

    var ok = await _tasks.DiscardPlanningAsync(parent.Id);

    Assert.True(ok);
    Assert.Null(await _tasks.GetByIdAsync(c1.Id));
    Assert.Null(await _tasks.GetByIdAsync(c2.Id));

    var parentLoaded = await _tasks.GetByIdAsync(parent.Id);
    Assert.Equal(TaskStatus.Manual, parentLoaded!.Status);
    Assert.Null(parentLoaded.PlanningSessionId);
    Assert.Null(parentLoaded.PlanningSessionToken);
    Assert.Null(parentLoaded.PlanningFinalizedAt);
}

[Fact]
public async Task DiscardPlanningAsync_OnNonPlanningTask_ReturnsFalse()
{
    var listId = await CreateListAsync();
    var task = MakeTask(listId, TaskStatus.Manual);
    await _tasks.AddAsync(task);

    var ok = await _tasks.DiscardPlanningAsync(task.Id);

    Assert.False(ok);
}
```

- [ ] **Step 2: Run; verify fail**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryPlanningTests.DiscardPlanning"`
Expected: FAIL.

- [ ] **Step 3: Implement**

```csharp
public async Task<bool> DiscardPlanningAsync(
    string parentId,
    CancellationToken ct = default)
{
    using var tx = await _context.Database.BeginTransactionAsync(ct);

    var parent = await _context.Tasks.FirstOrDefaultAsync(t => t.Id == parentId, ct);
    if (parent is null || parent.Status != TaskStatus.Planning)
    {
        await tx.RollbackAsync(ct);
        return false;
    }

    await _context.Tasks
        .Where(t => t.ParentTaskId == parentId && t.Status == TaskStatus.Draft)
        .ExecuteDeleteAsync(ct);

    parent.Status = TaskStatus.Manual;
    parent.PlanningSessionId = null;
    parent.PlanningSessionToken = null;
    parent.PlanningFinalizedAt = null;

    await _context.SaveChangesAsync(ct);
    await tx.CommitAsync(ct);
    return true;
}
```

- [ ] **Step 4: Run; verify pass**

Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs src/ClaudeDo.Data/Repositories/TaskRepository.cs
git commit -m "feat(data): TaskRepository.DiscardPlanningAsync"
```

---

## Task 12: Repository method `TryCompleteParentAsync` (auto-status hook)

**Files:**
- Create: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryParentCompletionTests.cs`
- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs`

- [ ] **Step 1: Create the test class**

Create `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryParentCompletionTests.cs`:

```csharp
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Tests.Infrastructure;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Repositories;

public sealed class TaskRepositoryParentCompletionTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly ClaudeDoDbContext _ctx;
    private readonly TaskRepository _tasks;
    private readonly ListRepository _lists;

    public TaskRepositoryParentCompletionTests()
    {
        _ctx = _db.CreateContext();
        _tasks = new TaskRepository(_ctx);
        _lists = new ListRepository(_ctx);
    }

    public void Dispose() { _ctx.Dispose(); _db.Dispose(); }

    private async Task<string> ListAsync()
    {
        var id = Guid.NewGuid().ToString();
        await _lists.AddAsync(new ListEntity { Id = id, Name = "L", CreatedAt = DateTime.UtcNow });
        return id;
    }

    private async Task<TaskEntity> PlannedParentAsync(string listId)
    {
        var parent = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "p",
            Status = TaskStatus.Planned,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
        };
        await _tasks.AddAsync(parent);
        return parent;
    }

    private async Task<TaskEntity> ChildAsync(string listId, string parentId, TaskStatus status)
    {
        var child = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "c",
            Status = status,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
            ParentTaskId = parentId,
        };
        await _tasks.AddAsync(child);
        return child;
    }
}
```

- [ ] **Step 2: Write failing tests**

Append inside the class:

```csharp
[Fact]
public async Task TryCompleteParentAsync_AllChildrenDone_ParentBecomesDone()
{
    var listId = await ListAsync();
    var parent = await PlannedParentAsync(listId);
    await ChildAsync(listId, parent.Id, TaskStatus.Done);
    await ChildAsync(listId, parent.Id, TaskStatus.Done);

    await _tasks.TryCompleteParentAsync(parent.Id);

    var loaded = await _tasks.GetByIdAsync(parent.Id);
    Assert.Equal(TaskStatus.Done, loaded!.Status);
    Assert.NotNull(loaded.FinishedAt);
}

[Fact]
public async Task TryCompleteParentAsync_OneFailedRestDone_ParentBecomesFailed()
{
    var listId = await ListAsync();
    var parent = await PlannedParentAsync(listId);
    await ChildAsync(listId, parent.Id, TaskStatus.Done);
    await ChildAsync(listId, parent.Id, TaskStatus.Failed);

    await _tasks.TryCompleteParentAsync(parent.Id);

    var loaded = await _tasks.GetByIdAsync(parent.Id);
    Assert.Equal(TaskStatus.Failed, loaded!.Status);
    Assert.NotNull(loaded.FinishedAt);
}

[Fact]
public async Task TryCompleteParentAsync_OneStillRunning_ParentStaysPlanned()
{
    var listId = await ListAsync();
    var parent = await PlannedParentAsync(listId);
    await ChildAsync(listId, parent.Id, TaskStatus.Done);
    await ChildAsync(listId, parent.Id, TaskStatus.Running);

    await _tasks.TryCompleteParentAsync(parent.Id);

    var loaded = await _tasks.GetByIdAsync(parent.Id);
    Assert.Equal(TaskStatus.Planned, loaded!.Status);
    Assert.Null(loaded.FinishedAt);
}

[Fact]
public async Task TryCompleteParentAsync_ChildStillDraft_ParentStaysPlanned()
{
    var listId = await ListAsync();
    var parent = await PlannedParentAsync(listId);
    await ChildAsync(listId, parent.Id, TaskStatus.Done);
    await ChildAsync(listId, parent.Id, TaskStatus.Draft);

    await _tasks.TryCompleteParentAsync(parent.Id);

    var loaded = await _tasks.GetByIdAsync(parent.Id);
    Assert.Equal(TaskStatus.Planned, loaded!.Status);
}

[Fact]
public async Task TryCompleteParentAsync_ParentIsNotPlanned_NoChange()
{
    var listId = await ListAsync();
    var parent = await PlannedParentAsync(listId);
    await _ctx.Database.ExecuteSqlRawAsync("UPDATE tasks SET status = 'planning' WHERE id = {0}", parent.Id);
    await ChildAsync(listId, parent.Id, TaskStatus.Done);

    await _tasks.TryCompleteParentAsync(parent.Id);

    var loaded = await _tasks.GetByIdAsync(parent.Id);
    Assert.Equal(TaskStatus.Planning, loaded!.Status);
}
```

- [ ] **Step 3: Run; verify fail**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryParentCompletionTests"`
Expected: FAIL.

- [ ] **Step 4: Implement**

In `#region Planning` of `TaskRepository.cs`:

```csharp
public async Task TryCompleteParentAsync(
    string parentId,
    CancellationToken ct = default)
{
    var parent = await _context.Tasks.FirstOrDefaultAsync(t => t.Id == parentId, ct);
    if (parent is null || parent.Status != TaskStatus.Planned) return;

    var children = await _context.Tasks
        .Where(t => t.ParentTaskId == parentId)
        .Select(t => t.Status)
        .ToListAsync(ct);

    if (children.Count == 0) return;

    bool allTerminal = children.All(s => s == TaskStatus.Done || s == TaskStatus.Failed);
    if (!allTerminal) return;

    bool anyFailed = children.Any(s => s == TaskStatus.Failed);
    parent.Status = anyFailed ? TaskStatus.Failed : TaskStatus.Done;
    parent.FinishedAt = DateTime.UtcNow;
    await _context.SaveChangesAsync(ct);
}
```

- [ ] **Step 5: Run; verify pass**

Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryParentCompletionTests.cs src/ClaudeDo.Data/Repositories/TaskRepository.cs
git commit -m "feat(data): TaskRepository.TryCompleteParentAsync"
```

---

## Task 13: Regression test — queue skips Drafts/Planning/Planned

**Files:**
- Modify: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs`

- [ ] **Step 1: Write the test**

Append to planning test class:

```csharp
[Fact]
public async Task GetNextQueuedAgentTask_SkipsDraftPlanningPlanned()
{
    var listId = await CreateListAsync();
    var agent = await _tags.GetOrCreateAsync("agent");

    async Task<TaskEntity> T(TaskStatus s, bool withTag, string? parent = null)
    {
        var t = MakeTask(listId, s, parentId: parent);
        await _tasks.AddAsync(t);
        if (withTag) await _tasks.AddTagAsync(t.Id, agent.Id);
        return t;
    }

    var planning = await T(TaskStatus.Planning, withTag: true);
    var planned = await T(TaskStatus.Planned, withTag: true);
    var draft = await T(TaskStatus.Draft, withTag: true, parent: planning.Id);
    var queued = await T(TaskStatus.Queued, withTag: true);

    var picked = await _tasks.GetNextQueuedAgentTaskAsync(DateTime.UtcNow);

    Assert.NotNull(picked);
    Assert.Equal(queued.Id, picked!.Id);
}
```

- [ ] **Step 2: Run**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryPlanningTests.GetNextQueuedAgentTask_SkipsDraftPlanningPlanned"`
Expected: PASS immediately (the existing query filters on `status = 'queued'`, so Planning/Planned/Draft are already excluded).

If this test fails, something is wrong with the queue filter — investigate before continuing.

- [ ] **Step 3: Commit**

```bash
git add tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs
git commit -m "test(data): queue skips Planning/Planned/Draft"
```

---

## Task 14: Regression test — Restrict cascade on parent delete

**Files:**
- Modify: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs`

- [ ] **Step 1: Write the test**

```csharp
[Fact]
public async Task DeleteAsync_ParentWithChildren_ThrowsOrDoesNotDelete()
{
    var listId = await CreateListAsync();
    var parent = MakeTask(listId, TaskStatus.Planning);
    await _tasks.AddAsync(parent);
    await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);

    // ExecuteDelete bypasses the FK check in EF but SQLite enforces it at the DB level
    // when foreign_keys = ON (which ClaudeDoDbContext enables).
    await Assert.ThrowsAsync<Microsoft.EntityFrameworkCore.DbUpdateException>(async () =>
    {
        await _tasks.DeleteAsync(parent.Id);
    });

    var stillThere = await _tasks.GetByIdAsync(parent.Id);
    Assert.NotNull(stillThere);
}
```

- [ ] **Step 2: Run**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRepositoryPlanningTests.DeleteAsync_ParentWithChildren"`
Expected: PASS.

**If this test fails** because `ExecuteDeleteAsync` succeeds: SQLite may not be enforcing FKs in the test environment. Diagnose:
- Confirm `ClaudeDoDbContext` sets `PRAGMA foreign_keys = ON` on connection open.
- If not enforced, we need to replace `ExecuteDeleteAsync` in `DeleteAsync` with an EF-tracked delete (`_context.Tasks.Remove(...); SaveChangesAsync()`) so EF enforces Restrict before SQL is sent.

If FK enforcement is the issue, add this helper to `ClaudeDoDbContext.OnConfiguring` (if not already there) or verify the existing pragma:
```csharp
optionsBuilder.UseSqlite(conn, opts => opts.CommandTimeout(30));
```
and ensure the connection string or `UseSqlite` callback issues `PRAGMA foreign_keys = ON`.

- [ ] **Step 3: Commit**

```bash
git add tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs
# include any DbContext fix if needed
git commit -m "test(data): parent delete with children is restricted"
```

---

## Task 15: Wire `TryCompleteParentAsync` into `TaskRunner`

**Files:**
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs`
- Create: `tests/ClaudeDo.Worker.Tests/Runner/TaskRunnerParentCompletionTests.cs` (integration)

- [ ] **Step 1: Locate the call-sites**

Open `src/ClaudeDo.Worker/Runner/TaskRunner.cs`. Three locations already call `MarkDoneAsync` / `MarkFailedAsync` (line numbers per spec reference: ~333, ~348, ~362).

At each location, the variable `task` (or `taskId` in the last two) is in scope. For the one at line ~333 `task` is the full `TaskEntity`; for the two failure paths, only `taskId` — but we can load the parent id from the repo.

- [ ] **Step 2: Update the Done-path (around line 333)**

Right after:
```csharp
await taskRepo.MarkDoneAsync(task.Id, finishedAt, result.ResultMarkdown, CancellationToken.None);
```
add:
```csharp
if (task.ParentTaskId is not null)
    await taskRepo.TryCompleteParentAsync(task.ParentTaskId, CancellationToken.None);
```

- [ ] **Step 3: Update the two Failed-paths (around lines 348, 362)**

For each, right after the `MarkFailedAsync` call, add a block that re-reads the task to get `ParentTaskId` (the scope has `taskId` not the entity):

```csharp
await taskRepo.MarkFailedAsync(taskId, finishedAt, result.ErrorMarkdown, CancellationToken.None);
var justFailed = await taskRepo.GetByIdAsync(taskId, CancellationToken.None);
if (justFailed?.ParentTaskId is not null)
    await taskRepo.TryCompleteParentAsync(justFailed.ParentTaskId, CancellationToken.None);
```

Do the same for the second failure path at line ~362.

- [ ] **Step 4: Build**

Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj`
Expected: builds.

- [ ] **Step 5: Write integration test**

Create `tests/ClaudeDo.Worker.Tests/Runner/TaskRunnerParentCompletionTests.cs`:

The intent is to exercise the full path: child task ends → parent updates. Rather than invoking the full `TaskRunner` (which requires Claude CLI), we test the repository integration by calling `MarkDoneAsync` + `TryCompleteParentAsync` in the same sequence the runner now does.

```csharp
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Tests.Infrastructure;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Runner;

public sealed class TaskRunnerParentCompletionTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly ClaudeDoDbContext _ctx;
    private readonly TaskRepository _tasks;
    private readonly ListRepository _lists;

    public TaskRunnerParentCompletionTests()
    {
        _ctx = _db.CreateContext();
        _tasks = new TaskRepository(_ctx);
        _lists = new ListRepository(_ctx);
    }

    public void Dispose() { _ctx.Dispose(); _db.Dispose(); }

    [Fact]
    public async Task ChildMarkedDone_LastOne_ParentFinalized()
    {
        var listId = Guid.NewGuid().ToString();
        await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });

        var parent = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "p",
            Status = TaskStatus.Planned,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
        };
        await _tasks.AddAsync(parent);

        var c1 = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "c1",
            Status = TaskStatus.Done,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
            ParentTaskId = parent.Id,
        };
        var c2 = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "c2",
            Status = TaskStatus.Running,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
            ParentTaskId = parent.Id,
        };
        await _tasks.AddAsync(c1);
        await _tasks.AddAsync(c2);

        // Simulate the runner finishing the second child:
        await _tasks.MarkDoneAsync(c2.Id, DateTime.UtcNow, "done");
        if (c2.ParentTaskId is not null)
            await _tasks.TryCompleteParentAsync(c2.ParentTaskId);

        var parentLoaded = await _tasks.GetByIdAsync(parent.Id);
        Assert.Equal(TaskStatus.Done, parentLoaded!.Status);
        Assert.NotNull(parentLoaded.FinishedAt);
    }
}
```

- [ ] **Step 6: Run; verify pass**

Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~TaskRunnerParentCompletionTests"`
Expected: PASS.

- [ ] **Step 7: Commit**

```bash
git add src/ClaudeDo.Worker/Runner/TaskRunner.cs tests/ClaudeDo.Worker.Tests/Runner/TaskRunnerParentCompletionTests.cs
git commit -m "feat(worker): hook TryCompleteParentAsync after MarkDone/MarkFailed"
```

---

## Task 16: Full test run and merge preparation

**Files:** none (verification step)

- [ ] **Step 1: Run the full test suite**

Run: `dotnet test tests/ClaudeDo.Worker.Tests`
Expected: all tests pass, no regressions.

- [ ] **Step 2: Verify build of every project**

Run:
```bash
dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj
```
Expected: all succeed.

- [ ] **Step 3: Verify the migration file is present for parallel-plan detection**

Run:
```bash
ls src/ClaudeDo.Data/Migrations/*AddPlanningSupport*.cs
```
Expected: exactly one file matches (the migration + designer file). This is the marker that Plans B and C wait for.

- [ ] **Step 4: Summarize what's done**

Plan A now delivers:
- Three new `TaskStatus` values (Planning/Planned/Draft) with persistence.
- Four new columns on `tasks` (`parent_task_id`, `planning_session_id`, `planning_session_token`, `planning_finalized_at`).
- Self-ref FK with `DeleteBehavior.Restrict`.
- Seven new repository methods.
- Auto-parent-completion hook wired into `TaskRunner`.
- Regression test that the queue still only picks `Queued`.
- Regression test that parent delete with children fails.

Nothing UI-visible changes yet; users notice only that deleting a task that has children now fails with a DB error (Plan C will fix the UX by adding a confirmation dialog).

---

## Out of scope for Plan A

- Any MCP server, SignalR endpoints, or terminal launcher → Plan B.
- Any UI rendering of hierarchy, context menu entries, or draft styling → Plan C.
- Session-folder file management (`~/.todo-app/planning-sessions/`) → Plan B.
- Resolving unknowns around Claude CLI flags (`--thinking-budget`, `--allowedTools` casing, session-ID capture) → Plan B.
