# Agent Settings UI Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Restore the ability to configure Model / SystemPrompt / AgentPath per list (via a new modal) and per task (via an expander in DetailsIsland), persisting through SignalR hub methods to the existing DB schema.

**Architecture:** UI → new `WorkerHub` methods (`UpdateList`, `UpdateListConfig`, `UpdateTaskAgentSettings`, `GetListConfig`) → existing repositories in `ClaudeDo.Data` (schema already in place). Worker broadcasts `ListUpdated` so the lists island refreshes. Per-task settings auto-save on change, debounced. The DB columns/tables already exist; `TaskRunner` + `ClaudeArgsBuilder` already consume them.

**Tech Stack:** .NET 8, Avalonia 12, CommunityToolkit.Mvvm, EF Core, SignalR (hub on `127.0.0.1:47821`), xUnit integration tests with real SQLite.

**Build tip (from project memory):** `dotnet build ClaudeDo.slnx` fails on .NET 8. Build individual csproj files:
```
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
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj
```

---

## File Structure

New files:
- `src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml`
- `src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml.cs`
- `src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs`
- `tests/ClaudeDo.Worker.Tests/Repositories/ListRepositoryDeleteConfigTests.cs`
- `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryAgentSettingsTests.cs`
- `tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs`

Modified files:
- `src/ClaudeDo.Data/Repositories/ListRepository.cs` — add `DeleteConfigAsync`
- `src/ClaudeDo.Data/Repositories/TaskRepository.cs` — add `UpdateAgentSettingsAsync`
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — add 4 methods + DTOs
- `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs` — add `ListUpdatedAsync`
- `src/ClaudeDo.Ui/Services/WorkerClient.cs` — add 4 methods + `ListUpdatedEvent`
- `src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs` (no changes expected, only for reference)
- `src/ClaudeDo.App/App.axaml.cs` (or DI extension file) — register `ListSettingsModalViewModel`
- `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml` — add context menu + gear button
- `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs` — add `OpenSettingsCommand`, subscribe `ListUpdated`
- `src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs` — expose list fields needed by modal
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` — add agent-settings fields + auto-save
- `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` — add "Agent settings" expander
- `src/ClaudeDo.Data/CLAUDE.md` — refresh with new repo methods + ListConfigEntity
- `src/ClaudeDo.Worker/CLAUDE.md` — document new hub methods + `ListUpdated`
- `src/ClaudeDo.Ui/CLAUDE.md` — document list settings modal + details expander

---

## Task 1: Add `ListRepository.DeleteConfigAsync`

**Files:**
- Modify: `src/ClaudeDo.Data/Repositories/ListRepository.cs`
- Test: `tests/ClaudeDo.Worker.Tests/Repositories/ListRepositoryDeleteConfigTests.cs`

The existing `ListRepository.SetConfigAsync` upserts but never deletes. The UI needs a way to fully remove a list config row (when the user clears all three agent fields).

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

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

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

namespace ClaudeDo.Worker.Tests.Repositories;

public sealed class ListRepositoryDeleteConfigTests : IAsyncLifetime
{
    private readonly TempDatabase _db = new();

    public async Task InitializeAsync() => await _db.InitializeAsync();
    public async Task DisposeAsync() => await _db.DisposeAsync();

    [Fact]
    public async Task DeleteConfigAsync_RemovesExistingRow()
    {
        await using var ctx = _db.CreateContext();
        var repo = new ListRepository(ctx);

        var listId = Guid.NewGuid().ToString();
        await repo.AddAsync(new ListEntity
        {
            Id = listId,
            Name = "L",
            CreatedAt = DateTime.UtcNow,
        });
        await repo.SetConfigAsync(new ListConfigEntity
        {
            ListId = listId,
            Model = "opus",
            SystemPrompt = "hello",
            AgentPath = "/tmp/a.md",
        });

        var removed = await repo.DeleteConfigAsync(listId);

        Assert.True(removed);
        Assert.Null(await repo.GetConfigAsync(listId));
    }

    [Fact]
    public async Task DeleteConfigAsync_ReturnsFalseWhenAbsent()
    {
        await using var ctx = _db.CreateContext();
        var repo = new ListRepository(ctx);

        var removed = await repo.DeleteConfigAsync(Guid.NewGuid().ToString());

        Assert.False(removed);
    }
}
```

> If `TempDatabase` / `Infrastructure` namespace differs, match the existing test infrastructure pattern seen in other `*RepositoryTests` files in `tests/ClaudeDo.Worker.Tests/Repositories/`. Read one existing test file first to confirm the exact helper class name and SQLite setup call.

- [ ] **Step 2: Run test to verify it fails**

```
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter "FullyQualifiedName~ListRepositoryDeleteConfigTests"
```
Expected: FAIL with `'ListRepository' does not contain a definition for 'DeleteConfigAsync'`.

- [ ] **Step 3: Add the method**

In `src/ClaudeDo.Data/Repositories/ListRepository.cs`, add below `SetConfigAsync`:

```csharp
public async Task<bool> DeleteConfigAsync(string listId, CancellationToken ct = default)
{
    var affected = await _context.ListConfigs
        .Where(c => c.ListId == listId)
        .ExecuteDeleteAsync(ct);
    return affected > 0;
}
```

- [ ] **Step 4: Run test to verify it passes**

```
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter "FullyQualifiedName~ListRepositoryDeleteConfigTests"
```
Expected: 2 tests passed.

- [ ] **Step 5: Commit**

```
git add src/ClaudeDo.Data/Repositories/ListRepository.cs tests/ClaudeDo.Worker.Tests/Repositories/ListRepositoryDeleteConfigTests.cs
git commit -m "feat(data): add ListRepository.DeleteConfigAsync"
```

---

## Task 2: Add `TaskRepository.UpdateAgentSettingsAsync`

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

Focused method so the UI can update the three override columns without loading the entity.

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

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

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

namespace ClaudeDo.Worker.Tests.Repositories;

public sealed class TaskRepositoryAgentSettingsTests : IAsyncLifetime
{
    private readonly TempDatabase _db = new();

    public async Task InitializeAsync() => await _db.InitializeAsync();
    public async Task DisposeAsync() => await _db.DisposeAsync();

    private async Task<string> SeedTaskAsync()
    {
        await using var ctx = _db.CreateContext();
        var listId = Guid.NewGuid().ToString();
        var taskId = Guid.NewGuid().ToString();
        await new ListRepository(ctx).AddAsync(new ListEntity
        {
            Id = listId, Name = "L", CreatedAt = DateTime.UtcNow,
        });
        await new TaskRepository(ctx).AddAsync(new TaskEntity
        {
            Id = taskId, ListId = listId, Title = "T", CreatedAt = DateTime.UtcNow,
        });
        return taskId;
    }

    [Fact]
    public async Task UpdateAgentSettingsAsync_SetsAllThreeFields()
    {
        var taskId = await SeedTaskAsync();

        await using var ctx = _db.CreateContext();
        var repo = new TaskRepository(ctx);

        await repo.UpdateAgentSettingsAsync(taskId, "opus", "system!", "/tmp/a.md");

        var entity = await repo.GetByIdAsync(taskId);
        Assert.NotNull(entity);
        Assert.Equal("opus", entity!.Model);
        Assert.Equal("system!", entity.SystemPrompt);
        Assert.Equal("/tmp/a.md", entity.AgentPath);
    }

    [Fact]
    public async Task UpdateAgentSettingsAsync_NullsClearColumns()
    {
        var taskId = await SeedTaskAsync();

        await using (var ctx = _db.CreateContext())
        {
            await new TaskRepository(ctx).UpdateAgentSettingsAsync(taskId, "opus", "s", "/a.md");
        }

        await using (var ctx = _db.CreateContext())
        {
            var repo = new TaskRepository(ctx);
            await repo.UpdateAgentSettingsAsync(taskId, null, null, null);

            var entity = await repo.GetByIdAsync(taskId);
            Assert.NotNull(entity);
            Assert.Null(entity!.Model);
            Assert.Null(entity.SystemPrompt);
            Assert.Null(entity.AgentPath);
        }
    }
}
```

- [ ] **Step 2: Run test to verify it fails**

```
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter "FullyQualifiedName~TaskRepositoryAgentSettingsTests"
```
Expected: FAIL with `'TaskRepository' does not contain a definition for 'UpdateAgentSettingsAsync'`.

- [ ] **Step 3: Add the method**

In `src/ClaudeDo.Data/Repositories/TaskRepository.cs`, add at the end of the `#region Status transitions` block (or a new `#region Agent settings` right after):

```csharp
public async Task UpdateAgentSettingsAsync(
    string taskId,
    string? model,
    string? systemPrompt,
    string? agentPath,
    CancellationToken ct = default)
{
    await _context.Tasks
        .Where(t => t.Id == taskId)
        .ExecuteUpdateAsync(s => s
            .SetProperty(t => t.Model, model)
            .SetProperty(t => t.SystemPrompt, systemPrompt)
            .SetProperty(t => t.AgentPath, agentPath), ct);
}
```

- [ ] **Step 4: Run test to verify it passes**

```
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter "FullyQualifiedName~TaskRepositoryAgentSettingsTests"
```
Expected: 2 tests passed.

- [ ] **Step 5: Commit**

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

---

## Task 3: Hub methods + DTOs + `ListUpdated` broadcast

**Files:**
- Modify: `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
- Test: `tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs`

Add four hub methods (`UpdateList`, `UpdateListConfig`, `UpdateTaskAgentSettings`, `GetListConfig`) plus a `ListUpdated` broadcast event.

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

Create `tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs`:

> Pattern notes: the worker test project usually calls hub methods by invoking repositories/services directly when no SignalR host is spun up. Read an existing hub-adjacent test (if one exists) to confirm. If no precedent, assert the underlying persistence via repo calls — the hub methods are thin wrappers.

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

namespace ClaudeDo.Worker.Tests.Hub;

// These tests exercise the hub behavior by invoking the underlying repository
// chain the hub calls. The hub itself is a thin wrapper around these repos;
// full SignalR integration testing is covered manually per the spec.
public sealed class AgentSettingsHubTests : IAsyncLifetime
{
    private readonly TempDatabase _db = new();
    public async Task InitializeAsync() => await _db.InitializeAsync();
    public async Task DisposeAsync() => await _db.DisposeAsync();

    [Fact]
    public async Task UpdateListConfig_AllNull_DeletesRow()
    {
        await using var ctx = _db.CreateContext();
        var repo = new ListRepository(ctx);

        var listId = Guid.NewGuid().ToString();
        await repo.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
        await repo.SetConfigAsync(new ListConfigEntity
        {
            ListId = listId, Model = "opus", SystemPrompt = null, AgentPath = null,
        });

        // Simulate hub: all three null => delete.
        const string? model = null; const string? sp = null; const string? ap = null;
        if (model is null && sp is null && ap is null)
            await repo.DeleteConfigAsync(listId);
        else
            await repo.SetConfigAsync(new ListConfigEntity
            {
                ListId = listId, Model = model, SystemPrompt = sp, AgentPath = ap,
            });

        Assert.Null(await repo.GetConfigAsync(listId));
    }
}
```

- [ ] **Step 2: Run test to verify it fails**

It actually passes since it only calls existing code. The purpose of this test is to encode the hub's "all-null → delete" contract so that Task 3's code changes don't break it.

```
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter "FullyQualifiedName~AgentSettingsHubTests"
```
Expected: 1 passed (the test is a spec/guardrail, not a red-green).

- [ ] **Step 3: Add the broadcaster event**

In `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`, add a new method alongside the existing `TaskUpdatedAsync`:

```csharp
public Task ListUpdatedAsync(string listId) =>
    _hub.Clients.All.SendAsync("ListUpdated", listId);
```

- [ ] **Step 4: Add DTOs + hub methods**

In `src/ClaudeDo.Worker/Hub/WorkerHub.cs`, add these `record` DTOs at the top (alongside `ActiveTaskDto`, `AppSettingsDto`, etc.):

```csharp
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath);
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath);
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath);
```

Then add these methods to the `WorkerHub` class (place at the end, before the closing brace):

```csharp
public async Task UpdateList(UpdateListDto dto)
{
    using var ctx = _dbFactory.CreateDbContext();
    var repo = new ListRepository(ctx);
    var entity = await repo.GetByIdAsync(dto.Id);
    if (entity is null) throw new HubException("list not found");

    entity.Name = dto.Name;
    entity.WorkingDir = string.IsNullOrWhiteSpace(dto.WorkingDir) ? null : dto.WorkingDir;
    entity.DefaultCommitType = string.IsNullOrWhiteSpace(dto.DefaultCommitType) ? "chore" : dto.DefaultCommitType;
    await repo.UpdateAsync(entity);

    await _broadcaster.ListUpdatedAsync(dto.Id);
}

public async Task UpdateListConfig(UpdateListConfigDto dto)
{
    using var ctx = _dbFactory.CreateDbContext();
    var repo = new ListRepository(ctx);

    var model = Nullify(dto.Model);
    var systemPrompt = Nullify(dto.SystemPrompt);
    var agentPath = Nullify(dto.AgentPath);

    if (model is null && systemPrompt is null && agentPath is null)
    {
        await repo.DeleteConfigAsync(dto.ListId);
    }
    else
    {
        await repo.SetConfigAsync(new ListConfigEntity
        {
            ListId = dto.ListId,
            Model = model,
            SystemPrompt = systemPrompt,
            AgentPath = agentPath,
        });
    }

    await _broadcaster.ListUpdatedAsync(dto.ListId);
}

public async Task<ListConfigDto?> GetListConfig(string listId)
{
    using var ctx = _dbFactory.CreateDbContext();
    var repo = new ListRepository(ctx);
    var config = await repo.GetConfigAsync(listId);
    if (config is null) return null;
    return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath);
}

public async Task UpdateTaskAgentSettings(UpdateTaskAgentSettingsDto dto)
{
    using var ctx = _dbFactory.CreateDbContext();
    var repo = new TaskRepository(ctx);
    await repo.UpdateAgentSettingsAsync(
        dto.TaskId,
        Nullify(dto.Model),
        Nullify(dto.SystemPrompt),
        Nullify(dto.AgentPath));

    await _broadcaster.TaskUpdatedAsync(dto.TaskId);
}

private static string? Nullify(string? s) => string.IsNullOrWhiteSpace(s) ? null : s;
```

If `_broadcaster.TaskUpdatedAsync` has a different signature in the existing broadcaster, adapt the call to match — read `HubBroadcaster.cs` for the exact name.

- [ ] **Step 5: Build worker**

```
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
```
Expected: build succeeded, 0 errors.

- [ ] **Step 6: Run the guardrail test**

```
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter "FullyQualifiedName~AgentSettingsHubTests"
```
Expected: passed.

- [ ] **Step 7: Commit**

```
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Worker/Hub/HubBroadcaster.cs tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs
git commit -m "feat(worker): add hub methods for list and task agent settings"
```

---

## Task 4: WorkerClient — 4 new methods + `ListUpdatedEvent`

**Files:**
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`

Add client-side DTOs (mirroring the hub DTOs, or import from `ClaudeDo.Worker` if cross-project references allow — else redeclare as simple records in the UI project), client methods, and subscribe to the new `ListUpdated` event.

- [ ] **Step 1: Inspect current WorkerClient shape**

Open `src/ClaudeDo.Ui/Services/WorkerClient.cs` and find where `UpdateAppSettings`, `TaskUpdatedEvent`, and the hub event subscriptions are declared. The new code will mirror the exact pattern used there (naming, event signature, async wrapper).

- [ ] **Step 2: Add client DTOs**

At the top of `WorkerClient.cs` (or in a nested namespace if the file already has DTO records, e.g. `AppSettingsDto`), add:

```csharp
public sealed record UpdateListClientDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
public sealed record UpdateListConfigClientDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath);
public sealed record UpdateTaskAgentSettingsClientDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath);
public sealed record ListConfigClientDto(string? Model, string? SystemPrompt, string? AgentPath);
```

Use the existing file's naming convention instead of "ClientDto" if the file already has a convention (e.g., `AppSettingsDto` with no suffix — then follow suit and just name them `UpdateListDto`, `UpdateListConfigDto`, etc. in the UI namespace).

- [ ] **Step 3: Add the four client methods**

Inside the `WorkerClient` class, alongside `UpdateAppSettings`:

```csharp
public async Task UpdateListAsync(UpdateListClientDto dto, CancellationToken ct = default)
{
    await _hub.InvokeAsync("UpdateList", dto, ct);
}

public async Task UpdateListConfigAsync(UpdateListConfigClientDto dto, CancellationToken ct = default)
{
    await _hub.InvokeAsync("UpdateListConfig", dto, ct);
}

public async Task<ListConfigClientDto?> GetListConfigAsync(string listId, CancellationToken ct = default)
{
    return await _hub.InvokeAsync<ListConfigClientDto?>("GetListConfig", listId, ct);
}

public async Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsClientDto dto, CancellationToken ct = default)
{
    await _hub.InvokeAsync("UpdateTaskAgentSettings", dto, ct);
}
```

- [ ] **Step 4: Subscribe to `ListUpdated` event**

Find where other hub events are subscribed (usually in `WorkerClient`'s `ConfigureHub` / `StartAsync` / constructor-equivalent setup). Add a line mirroring the pattern used for `TaskUpdated`:

```csharp
_hub.On<string>("ListUpdated", listId => ListUpdatedEvent?.Invoke(listId));
```

And declare the event alongside the other events (e.g. `TaskUpdatedEvent`):

```csharp
public event Action<string>? ListUpdatedEvent;
```

- [ ] **Step 5: Build UI**

```
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
```
Expected: build succeeded, 0 errors.

- [ ] **Step 6: Commit**

```
git add src/ClaudeDo.Ui/Services/WorkerClient.cs
git commit -m "feat(ui): WorkerClient supports list/task agent settings + ListUpdated event"
```

---

## Task 5: `ListSettingsModalViewModel`

**Files:**
- Create: `src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs`
- Modify: `src/ClaudeDo.App/App.axaml.cs` (or wherever DI is registered — look for `AddTransient<WorktreeModalViewModel>` / `AddSingleton<MergeModalViewModel>` to find the registration block)

- [ ] **Step 1: Create the ViewModel**

Write `src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs`:

```csharp
using System.Collections.ObjectModel;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace ClaudeDo.Ui.ViewModels.Modals;

public sealed partial class ListSettingsModalViewModel : ViewModelBase
{
    private readonly WorkerClient _worker;

    // Set by caller before Show
    public string ListId { get; set; } = "";

    [ObservableProperty] private string _name = "";
    [ObservableProperty] private string _workingDir = "";
    [ObservableProperty] private string _defaultCommitType = "chore";

    [ObservableProperty] private string _selectedModel = "(default)";
    [ObservableProperty] private string _systemPrompt = "";
    [ObservableProperty] private AgentInfo? _selectedAgent;

    public ObservableCollection<string> ModelOptions { get; } = new()
    {
        "(default)", "sonnet", "opus", "haiku",
    };

    public ObservableCollection<string> CommitTypeOptions { get; } = new()
    {
        "chore", "feat", "fix", "refactor", "docs", "test", "ci", "perf", "style", "build",
    };

    public ObservableCollection<AgentInfo> Agents { get; } = new();

    public Action? CloseAction { get; set; }

    public ListSettingsModalViewModel(WorkerClient worker)
    {
        _worker = worker;
    }

    public async Task LoadAsync(
        string listId,
        string name,
        string? workingDir,
        string defaultCommitType,
        CancellationToken ct = default)
    {
        ListId = listId;
        Name = name;
        WorkingDir = workingDir ?? "";
        DefaultCommitType = string.IsNullOrWhiteSpace(defaultCommitType) ? "chore" : defaultCommitType;

        Agents.Clear();
        Agents.Add(new AgentInfo("(none)", "", ""));
        var agents = await _worker.GetAgentsAsync();
        foreach (var a in agents) Agents.Add(a);

        var config = await _worker.GetListConfigAsync(listId, ct);
        SelectedModel = string.IsNullOrWhiteSpace(config?.Model) ? "(default)" : config!.Model!;
        SystemPrompt = config?.SystemPrompt ?? "";
        SelectedAgent = string.IsNullOrWhiteSpace(config?.AgentPath)
            ? Agents[0]
            : (Agents.FirstOrDefault(a => a.Path == config!.AgentPath) ?? Agents[0]);
    }

    [RelayCommand]
    private async Task SaveAsync()
    {
        var model = SelectedModel == "(default)" ? null : SelectedModel;
        var sp = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt;
        var ap = SelectedAgent is null || string.IsNullOrWhiteSpace(SelectedAgent.Path) ? null : SelectedAgent.Path;

        await _worker.UpdateListAsync(new UpdateListClientDto(
            ListId,
            string.IsNullOrWhiteSpace(Name) ? "Untitled" : Name,
            string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir,
            DefaultCommitType));

        await _worker.UpdateListConfigAsync(new UpdateListConfigClientDto(
            ListId, model, sp, ap));

        CloseAction?.Invoke();
    }

    [RelayCommand]
    private void Cancel() => CloseAction?.Invoke();

    [RelayCommand]
    private void ResetAgentSettings()
    {
        SelectedModel = "(default)";
        SystemPrompt = "";
        SelectedAgent = Agents.Count > 0 ? Agents[0] : null;
    }
}
```

> Match the exact DTO record names used in `WorkerClient.cs` from Task 4. If you named them `UpdateListDto` (without `Client` suffix), update the `new UpdateListClientDto(...)` calls above to `new UpdateListDto(...)`.

- [ ] **Step 2: Register in DI**

Find the file that registers modal ViewModels (search the repo for `AddTransient<WorktreeModalViewModel>` or `AddSingleton<MergeModalViewModel>` — likely in `src/ClaudeDo.App/App.axaml.cs` or a DI extension). Add:

```csharp
services.AddTransient<ListSettingsModalViewModel>();
```

- [ ] **Step 3: Build UI**

```
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj
```
Expected: both build succeeded, 0 errors.

- [ ] **Step 4: Commit**

```
git add src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs src/ClaudeDo.App/App.axaml.cs
git commit -m "feat(ui): add ListSettingsModalViewModel"
```

---

## Task 6: `ListSettingsModalView`

**Files:**
- Create: `src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml`
- Create: `src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml.cs`

- [ ] **Step 1: Create the view XAML**

Write `src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml`:

```xml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
        x:Class="ClaudeDo.Ui.Views.Modals.ListSettingsModalView"
        x:DataType="vm:ListSettingsModalViewModel"
        Title="List settings"
        Width="520" Height="600"
        WindowStartupLocation="CenterOwner"
        CanResize="False">
    <DockPanel Margin="16">
        <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8" Margin="0,16,0,0">
            <Button Content="Cancel" Command="{Binding CancelCommand}" />
            <Button Content="Save" Command="{Binding SaveCommand}" Classes="accent" />
        </StackPanel>

        <ScrollViewer>
            <StackPanel Spacing="16">
                <TextBlock Text="General" FontSize="16" FontWeight="SemiBold" />

                <StackPanel Spacing="4">
                    <TextBlock Text="Name" />
                    <TextBox Text="{Binding Name}" />
                </StackPanel>

                <StackPanel Spacing="4">
                    <TextBlock Text="Working directory" />
                    <Grid ColumnDefinitions="*,Auto" >
                        <TextBox Grid.Column="0" Text="{Binding WorkingDir}" Watermark="(none)" />
                        <Button Grid.Column="1" Content="Browse…" Margin="8,0,0,0" Click="BrowseClicked" />
                    </Grid>
                </StackPanel>

                <StackPanel Spacing="4">
                    <TextBlock Text="Default commit type" />
                    <ComboBox ItemsSource="{Binding CommitTypeOptions}"
                              SelectedItem="{Binding DefaultCommitType, Mode=TwoWay}"
                              HorizontalAlignment="Left" MinWidth="160" />
                </StackPanel>

                <Separator Margin="0,8,0,8" />

                <Grid ColumnDefinitions="*,Auto">
                    <TextBlock Grid.Column="0" Text="Agent" FontSize="16" FontWeight="SemiBold" />
                    <Button Grid.Column="1" Content="Reset agent settings"
                            Command="{Binding ResetAgentSettingsCommand}" />
                </Grid>

                <StackPanel Spacing="4">
                    <TextBlock Text="Model" />
                    <ComboBox ItemsSource="{Binding ModelOptions}"
                              SelectedItem="{Binding SelectedModel, Mode=TwoWay}"
                              HorizontalAlignment="Left" MinWidth="160" />
                </StackPanel>

                <StackPanel Spacing="4">
                    <TextBlock Text="System prompt (appended)" />
                    <TextBox Text="{Binding SystemPrompt, Mode=TwoWay}"
                             AcceptsReturn="True" TextWrapping="Wrap"
                             MinHeight="80" />
                </StackPanel>

                <StackPanel Spacing="4">
                    <TextBlock Text="Agent file" />
                    <ComboBox ItemsSource="{Binding Agents}"
                              SelectedItem="{Binding SelectedAgent, Mode=TwoWay}"
                              HorizontalAlignment="Left" MinWidth="240">
                        <ComboBox.ItemTemplate>
                            <DataTemplate>
                                <StackPanel>
                                    <TextBlock Text="{Binding Name}" />
                                    <TextBlock Text="{Binding Description}" Opacity="0.6" FontSize="11" />
                                </StackPanel>
                            </DataTemplate>
                        </ComboBox.ItemTemplate>
                    </ComboBox>
                </StackPanel>
            </StackPanel>
        </ScrollViewer>
    </DockPanel>
</Window>
```

- [ ] **Step 2: Create the code-behind**

Write `src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml.cs`:

```csharp
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using ClaudeDo.Ui.ViewModels.Modals;

namespace ClaudeDo.Ui.Views.Modals;

public partial class ListSettingsModalView : Window
{
    public ListSettingsModalView()
    {
        InitializeComponent();
    }

    private async void BrowseClicked(object? sender, RoutedEventArgs e)
    {
        if (DataContext is not ListSettingsModalViewModel vm) return;
        var top = TopLevel.GetTopLevel(this);
        if (top is null) return;

        var folders = await top.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
        {
            Title = "Choose working directory",
            AllowMultiple = false,
        });
        if (folders.Count > 0)
        {
            vm.WorkingDir = folders[0].Path.LocalPath;
        }
    }
}
```

- [ ] **Step 3: Build UI**

```
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
```
Expected: build succeeded, 0 errors.

- [ ] **Step 4: Commit**

```
git add src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml.cs
git commit -m "feat(ui): add ListSettingsModalView"
```

---

## Task 7: Wire context menu + gear button in `ListsIslandView`

**Files:**
- Modify: `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml`
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs`
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs` (expose fields needed by modal)

- [ ] **Step 1: Ensure `ListNavItemViewModel` carries list fields**

Read `src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs`. Confirm it has `Id`, `Name`. Add `WorkingDir` (string?) and `DefaultCommitType` (string) as `[ObservableProperty]` fields if missing. Also ensure the loader that populates `ListNavItemViewModel`s reads these from the DB.

- [ ] **Step 2: Add `OpenSettingsAsync` command to `ListsIslandViewModel`**

Read `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs`. Add a field `ShowListSettingsModal` (view wires it) and a command:

```csharp
// Set by the view so the command can show the modal as a dialog.
public Func<ListSettingsModalViewModel, System.Threading.Tasks.Task>? ShowListSettingsModal { get; set; }

[RelayCommand]
private async System.Threading.Tasks.Task OpenSettingsAsync(ListNavItemViewModel? row)
{
    if (row is null || ShowListSettingsModal is null) return;
    var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
    await vm.LoadAsync(row.Id, row.Name, row.WorkingDir, row.DefaultCommitType);
    await ShowListSettingsModal(vm);
    // Refresh this row from DB in case name/working-dir changed
    await RefreshRowAsync(row.Id);
}
```

Add an `_services` constructor dependency (`IServiceProvider`) if not already present. Add `RefreshRowAsync(string listId)` that re-queries the list entity and updates the row's observable fields. Also subscribe to `_worker.ListUpdatedEvent += id => _ = RefreshRowAsync(id);` in the constructor (dispatch to UI thread if needed per existing pattern).

- [ ] **Step 3: Update `ListsIslandView.axaml`**

Open `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml` and find the DataTemplate for list rows. Add a context menu + a gear button. Example additions (adapt to the current `DataTemplate`/`ItemsControl` structure):

```xml
<Grid ColumnDefinitions="*,Auto">
    <!-- existing name cell -->
    <TextBlock Grid.Column="0" Text="{Binding Name}" VerticalAlignment="Center" />
    <!-- NEW gear button, visible on hover via opacity trigger or always visible -->
    <Button Grid.Column="1"
            Content="⚙"
            ToolTip.Tip="Settings…"
            Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenSettingsCommand}"
            CommandParameter="{Binding}" />
    <Grid.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Settings…"
                      Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenSettingsCommand}"
                      CommandParameter="{Binding}" />
        </ContextMenu>
    </Grid.ContextMenu>
</Grid>
```

The exact ancestor navigation syntax depends on how other commands are bound in this view — match whatever pattern `ListsIslandView` already uses for its commands.

Also wire `ShowListSettingsModal` in `ListsIslandView.axaml.cs` `OnDataContextChanged` (mirror how other islands set their modal-show callbacks):

```csharp
if (DataContext is ListsIslandViewModel vm)
{
    vm.ShowListSettingsModal = async modal =>
    {
        var window = new ListSettingsModalView { DataContext = modal };
        modal.CloseAction = () => window.Close();
        var top = TopLevel.GetTopLevel(this) as Window;
        if (top is null) window.Show();
        else await window.ShowDialog(top);
    };
}
```

- [ ] **Step 4: Build**

```
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj
```
Expected: 0 errors.

- [ ] **Step 5: Commit**

```
git add src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs
git commit -m "feat(ui): open ListSettingsModal via context menu and gear button"
```

---

## Task 8: Per-task agent settings in `DetailsIsland`

**Files:**
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
- Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`

Per-task overrides with inherit-fallback display. Auto-save on change (debounced 300ms).

- [ ] **Step 1: Extend `DetailsIslandViewModel`**

In `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`, add (alongside the existing `_model` field):

```csharp
[ObservableProperty] private string _taskModelSelection = "(inherit)";
[ObservableProperty] private string _taskSystemPrompt = "";
[ObservableProperty] private AgentInfo? _taskSelectedAgent;

[ObservableProperty] private string _effectiveModelHint = "";
[ObservableProperty] private string _effectiveSystemPromptHint = "";
[ObservableProperty] private string _effectiveAgentHint = "";

public ObservableCollection<string> TaskModelOptions { get; } = new()
{
    "(inherit)", "sonnet", "opus", "haiku",
};

public ObservableCollection<AgentInfo> TaskAgentOptions { get; } = new();

private bool _suppressAgentSave;
private CancellationTokenSource? _agentSaveCts;
```

Add an async helper `LoadAgentSettingsAsync(TaskEntity taskEntity)` called from `BindAsync` *after* the entity is loaded:

```csharp
private async Task LoadAgentSettingsAsync(TaskEntity entity, CancellationToken ct)
{
    _suppressAgentSave = true;
    try
    {
        TaskAgentOptions.Clear();
        TaskAgentOptions.Add(new AgentInfo("(inherit)", "", ""));
        var agents = await _worker.GetAgentsAsync();
        foreach (var a in agents) TaskAgentOptions.Add(a);

        TaskModelSelection = string.IsNullOrWhiteSpace(entity.Model) ? "(inherit)" : entity.Model!;
        TaskSystemPrompt = entity.SystemPrompt ?? "";
        TaskSelectedAgent = string.IsNullOrWhiteSpace(entity.AgentPath)
            ? TaskAgentOptions[0]
            : (TaskAgentOptions.FirstOrDefault(a => a.Path == entity.AgentPath) ?? TaskAgentOptions[0]);

        // Effective-value hints — load list's config for display
        var listCfg = await _worker.GetListConfigAsync(entity.ListId, ct);
        EffectiveModelHint = listCfg?.Model ?? "(global default)";
        EffectiveSystemPromptHint = string.IsNullOrWhiteSpace(listCfg?.SystemPrompt) ? "(none)" : listCfg!.SystemPrompt!;
        EffectiveAgentHint = string.IsNullOrWhiteSpace(listCfg?.AgentPath) ? "(none)" : System.IO.Path.GetFileName(listCfg!.AgentPath!);
    }
    finally
    {
        _suppressAgentSave = false;
    }
}
```

Add `partial void` handlers that trigger a debounced save:

```csharp
partial void OnTaskModelSelectionChanged(string value) => QueueAgentSave();
partial void OnTaskSystemPromptChanged(string value) => QueueAgentSave();
partial void OnTaskSelectedAgentChanged(AgentInfo? value) => QueueAgentSave();

private void QueueAgentSave()
{
    if (_suppressAgentSave || Task is null) return;
    _agentSaveCts?.Cancel();
    _agentSaveCts = new CancellationTokenSource();
    var ct = _agentSaveCts.Token;
    _ = SaveAgentSettingsAsync(ct);
}

private async Task SaveAgentSettingsAsync(CancellationToken ct)
{
    try
    {
        await Task.Delay(300, ct);
        if (Task is null) return;

        var model = TaskModelSelection == "(inherit)" ? null : TaskModelSelection;
        var sp = string.IsNullOrWhiteSpace(TaskSystemPrompt) ? null : TaskSystemPrompt;
        var ap = TaskSelectedAgent is null || string.IsNullOrWhiteSpace(TaskSelectedAgent.Path)
            ? null : TaskSelectedAgent.Path;

        await _worker.UpdateTaskAgentSettingsAsync(
            new UpdateTaskAgentSettingsClientDto(Task.Id, model, sp, ap), ct);
    }
    catch (OperationCanceledException) { /* a newer change superseded this one */ }
    catch { /* best-effort; live log will surface worker errors */ }
}
```

Also add a computed `bool IsAgentSectionEnabled => !IsRunning;` (already partial for `IsRunning`; raise property change when it flips by adding to the existing `OnAgentStatusLabelChanged` method):

```csharp
OnPropertyChanged(nameof(IsAgentSectionEnabled));
```

> Note: `System.Threading.Tasks.Task.Delay` — the file uses `Task` as an `[ObservableProperty]` name so `Task.Delay` resolves to the property, not the type. Fully qualify as `await System.Threading.Tasks.Task.Delay(300, ct);`.

- [ ] **Step 2: Add the expander to `DetailsIslandView.axaml`**

Open the view and add, somewhere inside the main scroll/stack layout (after the existing notes / before or after other sections — pick a natural location in the current layout):

```xml
<Expander Header="Agent settings (overrides)"
          IsExpanded="False"
          IsEnabled="{Binding IsAgentSectionEnabled}">
    <StackPanel Spacing="8" Margin="0,8,0,0">

        <StackPanel Spacing="2">
            <TextBlock Text="Model" />
            <ComboBox ItemsSource="{Binding TaskModelOptions}"
                      SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}"
                      MinWidth="160" HorizontalAlignment="Left" />
            <TextBlock Text="{Binding EffectiveModelHint, StringFormat='Effective if inherited: {0}'}"
                       Opacity="0.6" FontSize="11" />
        </StackPanel>

        <StackPanel Spacing="2">
            <TextBlock Text="System prompt (appended)" />
            <TextBox Text="{Binding TaskSystemPrompt, Mode=TwoWay}"
                     AcceptsReturn="True" TextWrapping="Wrap" MinHeight="60"
                     Watermark="{Binding EffectiveSystemPromptHint}" />
        </StackPanel>

        <StackPanel Spacing="2">
            <TextBlock Text="Agent file" />
            <ComboBox ItemsSource="{Binding TaskAgentOptions}"
                      SelectedItem="{Binding TaskSelectedAgent, Mode=TwoWay}"
                      MinWidth="240" HorizontalAlignment="Left">
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Name}" />
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>
            <TextBlock Text="{Binding EffectiveAgentHint, StringFormat='Effective if inherited: {0}'}"
                       Opacity="0.6" FontSize="11" />
        </StackPanel>

    </StackPanel>
</Expander>
```

- [ ] **Step 3: Call `LoadAgentSettingsAsync` from `BindAsync`**

In `DetailsIslandViewModel.BindAsync`, after `AgentStatusLabel = entity.Status.ToString();`, add:

```csharp
await LoadAgentSettingsAsync(entity, ct);
ct.ThrowIfCancellationRequested();
```

In `Bind(TaskRowViewModel? row)` — the `row == null` branch — reset the new fields:

```csharp
TaskModelSelection = "(inherit)";
TaskSystemPrompt = "";
TaskSelectedAgent = null;
EffectiveModelHint = "";
EffectiveSystemPromptHint = "";
EffectiveAgentHint = "";
```

- [ ] **Step 4: Build**

```
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj
```
Expected: 0 errors.

- [ ] **Step 5: Commit**

```
git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
git commit -m "feat(ui): per-task agent settings in DetailsIsland"
```

---

## Task 9: Documentation refresh

**Files:**
- Modify: `src/ClaudeDo.Data/CLAUDE.md`
- Modify: `src/ClaudeDo.Worker/CLAUDE.md`
- Modify: `src/ClaudeDo.Ui/CLAUDE.md`

The project CLAUDE.md files are stale — `Data/CLAUDE.md` doesn't mention `ListConfigEntity`, `TaskRunEntity`, or the Model/SystemPrompt/AgentPath fields that already exist. Bring them up to date now that the full per-task/per-list path is wired end-to-end.

- [ ] **Step 1: Update `ClaudeDo.Data/CLAUDE.md`**

Add to the Models section:
```
- **ListConfigEntity** — ListId (PK, 1:1 with list), Model, SystemPrompt, AgentPath (all nullable)
- **TaskRunEntity** — per-run record (session_id, tokens, turns, result, structured output, exit code, log path)
- **SubtaskEntity**, **AgentInfo** — existing helpers
- **TaskEntity** — add Model / SystemPrompt / AgentPath override fields (nullable), IsStarred, IsMyDay, Notes
```

Add to Repositories section:
```
- **ListRepository** — also manages `list_config` via `GetConfigAsync`, `SetConfigAsync` (upsert), `DeleteConfigAsync`
- **TaskRepository** — also has `UpdateAgentSettingsAsync` (model/system-prompt/agent-path)
- **TaskRunRepository**, **SubtaskRepository**, **AppSettingsRepository** — existing
```

Schema section: bump to 9+ tables including `list_config`, `task_runs`, `subtasks`, `app_settings`.

- [ ] **Step 2: Update `ClaudeDo.Worker/CLAUDE.md`**

In the SignalR Hub section, extend the method list:
```
**WorkerHub** methods: Ping, GetActive, RunNow, CancelTask, WakeQueue, ContinueTask, ResetTask, GetAgents, RefreshAgents,
GetAppSettings, UpdateAppSettings, CleanupFinishedWorktrees, ResetAllWorktrees, MergeTask, GetMergeTargets,
UpdateList, UpdateListConfig, GetListConfig, UpdateTaskAgentSettings
```

In events list, add `ListUpdated(listId)` alongside `TaskUpdated`.

- [ ] **Step 3: Update `ClaudeDo.Ui/CLAUDE.md`**

In the "Views" section, add:
```
- **ListSettingsModalView** — edits list name, working dir, default commit type, and per-list Model/SystemPrompt/AgentPath. Opened via context menu or gear button on a list row.
- **DetailsIslandView** — now contains an "Agent settings (overrides)" expander with per-task Model/SystemPrompt/AgentPath, showing inherited effective values. Disabled while task is running.
```

In the "Services" section, extend WorkerClient method list to include `UpdateListAsync`, `UpdateListConfigAsync`, `GetListConfigAsync`, `UpdateTaskAgentSettingsAsync`. Events: add `ListUpdatedEvent`.

- [ ] **Step 4: Commit**

```
git add src/ClaudeDo.Data/CLAUDE.md src/ClaudeDo.Worker/CLAUDE.md src/ClaudeDo.Ui/CLAUDE.md
git commit -m "docs: refresh CLAUDE.md files for agent settings UI"
```

---

## Task 10: Full build + test + manual smoke checklist

- [ ] **Step 1: Full build of all non-installer projects**

```
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 four build with 0 errors.

- [ ] **Step 2: Run full test suite**

```
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj
```
Expected: all tests pass.

- [ ] **Step 3: Record manual smoke-test checklist**

This step is documentation only — the smoke test must be run by a human after merging (UI). Record the following in the PR description (or a file like `docs/smoke-tests/2026-04-22-agent-settings.md`):

1. Open ClaudeDo.App.
2. Right-click a list → **Settings…** opens modal.
3. Enter a Name, choose a Model (e.g. `opus`), enter a system prompt, pick an agent → Save.
4. Reopen modal → values persist.
5. Select a task in that list → DetailsIsland shows "Agent settings (overrides)" expander, expand it, confirm fields show `(inherit)` and the hint line reflects the list's chosen Model.
6. Set task Model to `haiku` → run the task → verify log shows `--model haiku` was passed.
7. Click **Reset agent settings** in list modal → Save → DetailsIsland now shows `(global default)` hint.
8. Start a task run → verify DetailsIsland agent expander is disabled while Running.

- [ ] **Step 4: Final status check**

```
git status
git log --oneline -15
```
Expected: working tree clean, 8 feature/test commits since the start + 1 doc commit + 1 spec commit.

---

## Notes for the implementing subagents

- **Sonnet** only (project preference).
- **Minimal diff**: do NOT refactor unrelated code. Only the files listed per task.
- **No summaries, no comments** added to unchanged code — the user dislikes over-commenting.
- If the project's file/naming pattern diverges from what's shown (e.g. DI registration file, hub broadcaster signature), match the existing pattern. The plan's code is illustrative but naming alignment with the codebase overrides it.
- If a test file references helpers (`TempDatabase`) that don't exist, mirror the helper actually used in the project's other `*RepositoryTests`. Do not invent new test infrastructure.
- Commit after each completed task (as listed in the plan).
