Files
ClaudeDo/docs/superpowers/plans/2026-06-04-inherited-settings-and-turns.md
2026-06-04 12:12:37 +02:00

50 KiB

Inherited Settings Display, Overrides, and Turns — 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: Make inherited config values visible in-place with a source-aware marker (inherited · List / inherited · Global), let users override them, and add Max turns as a list- and task-level overridable setting.

Architecture: Add a nullable MaxTurns override to ListConfigEntity and TaskEntity resolved task → list → global in TaskRunner. Thread it through repositories, SignalR DTOs, the SignalR hub, and the MCP config tools. In the UI, a pure resolution helper computes (value, source); a small reusable badge control plus per-field reset buttons render the marker in both the List settings modal and the task overrides flyout.

Tech Stack: .NET 8, EF Core (SQLite), ASP.NET Core SignalR, Avalonia 12 + CommunityToolkit.Mvvm, xUnit. ModelContextProtocol MCP server.

Build/test reminders (see CLAUDE.md): build individual csproj with -c Release (a running Worker locks Debug). Subagents use the sonnet model and stage files explicitly by path — never git add -A. locales/en.json and locales/de.json keys must stay in parity. Changing IWorkerClient / DTOs breaks hand-rolled fakes — update tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs.


File Structure

Created:

  • src/ClaudeDo.Data/Migrations/<timestamp>_InheritableMaxTurns.cs (+ .Designer.cs, generated by dotnet ef)
  • src/ClaudeDo.Ui/Services/InheritanceResolver.cs — pure helper: (value, source) resolution + InheritSource enum
  • src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml (+ .axaml.cs) — label + source badge control
  • tests/ClaudeDo.Ui.Tests/InheritanceResolverTests.cs
  • tests/ClaudeDo.Worker.Tests/Runner/MaxTurnsResolutionTests.cs (or extend an existing runner-config test file)
  • tests/ClaudeDo.Data.Tests/MaxTurnsRoundTripTests.cs

Modified:

  • src/ClaudeDo.Data/Models/ListConfigEntity.cs, TaskEntity.cs
  • src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs, TaskEntityConfiguration.cs
  • src/ClaudeDo.Data/Repositories/ListRepository.cs, TaskRepository.cs
  • src/ClaudeDo.Worker/Runner/TaskRunner.cs
  • src/ClaudeDo.Worker/Hub/WorkerHub.cs
  • src/ClaudeDo.Worker/External/ConfigMcpTools.cs
  • src/ClaudeDo.Ui/Services/WorkerClient.cs, Services/Interfaces/IWorkerClient.cs
  • tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
  • src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs, Views/Modals/ListSettingsModalView.axaml
  • src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs, Views/Islands/DetailsIslandView.axaml
  • locales/en.json, locales/de.json

Task 1: Add MaxTurns to entities, EF config, and migration

Files:

  • Modify: src/ClaudeDo.Data/Models/ListConfigEntity.cs

  • Modify: src/ClaudeDo.Data/Models/TaskEntity.cs

  • Modify: src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs

  • Modify: src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs:83-85

  • Create: migration (generated)

  • Step 1: Add the property to ListConfigEntity

In src/ClaudeDo.Data/Models/ListConfigEntity.cs, after the AgentPath line:

public sealed class ListConfigEntity
{
    public required string ListId { get; init; }
    public string? Model { get; set; }
    public string? SystemPrompt { get; set; }
    public string? AgentPath { get; set; }
    public int? MaxTurns { get; set; }

    // Navigation property
    public ListEntity List { get; set; } = null!;
}
  • Step 2: Add the property to TaskEntity

In src/ClaudeDo.Data/Models/TaskEntity.cs, beside the existing Model / SystemPrompt / AgentPath override properties, add:

    public int? MaxTurns { get; set; }
  • Step 3: Map the columns in both entity configurations

In ListConfigEntityConfiguration.cs, after the AgentPath mapping:

        builder.Property(c => c.MaxTurns).HasColumnName("max_turns");

In TaskEntityConfiguration.cs, after line 85 (AgentPath mapping):

        builder.Property(t => t.MaxTurns).HasColumnName("max_turns");
  • Step 4: Generate the migration

Run:

dotnet ef migrations add InheritableMaxTurns --project src/ClaudeDo.Data --startup-project src/ClaudeDo.Worker

Expected: creates src/ClaudeDo.Data/Migrations/<timestamp>_InheritableMaxTurns.cs (+ Designer) and updates ClaudeDoDbContextModelSnapshot.cs. The Up method should AddColumn<int>("max_turns", "list_config", nullable: true) and AddColumn<int>("max_turns", "tasks", nullable: true).

If the dotnet ef tool is unavailable, hand-author the migration mirroring Migrations/20260603141020_DailyPrepMaxTasks.cs but with nullable: true and no defaultValue, adding both columns, and add matching Property<int?>("MaxTurns").HasColumnName("max_turns") entries to the snapshot for the ListConfigEntity and TaskEntity builders.

  • Step 5: Verify the migration is sane

Open the generated _InheritableMaxTurns.cs and confirm both AddColumn calls use nullable: true and there is no defaultValue (null = inherit). Confirm Down drops both columns.

  • Step 6: Build Data

Run: dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj -c Release Expected: build succeeds.

  • Step 7: Commit
git add src/ClaudeDo.Data/Models/ListConfigEntity.cs src/ClaudeDo.Data/Models/TaskEntity.cs src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs src/ClaudeDo.Data/Migrations/
git commit -m "feat(data): add nullable max_turns override to list_config and tasks"

Task 2: Repositories persist MaxTurns

Files:

  • Modify: src/ClaudeDo.Data/Repositories/ListRepository.cs:56-70

  • Modify: src/ClaudeDo.Data/Repositories/TaskRepository.cs:157-170

  • Test: tests/ClaudeDo.Data.Tests/MaxTurnsRoundTripTests.cs

  • Step 1: Write the failing test

Create tests/ClaudeDo.Data.Tests/MaxTurnsRoundTripTests.cs. Use the same in-memory/temp-SQLite context setup as a neighboring Data.Tests file (open one, e.g. ListRepositoryTests.cs, and copy its context-creation helper exactly — it builds a ClaudeDoDbContext against a temp SQLite file and calls Database.Migrate() or EnsureCreated()).

using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using Xunit;

namespace ClaudeDo.Data.Tests;

public class MaxTurnsRoundTripTests
{
    [Fact]
    public async Task ListConfig_persists_max_turns()
    {
        await using var ctx = TestDb.NewContext(); // use the helper pattern from existing Data.Tests
        var lists = new ListRepository(ctx);
        await ctx.Lists.AddAsync(new ListEntity { Id = "L1", Name = "L1" });
        await ctx.SaveChangesAsync();

        await lists.SetConfigAsync(new ListConfigEntity { ListId = "L1", MaxTurns = 42 });

        var cfg = await lists.GetConfigAsync("L1");
        Assert.Equal(42, cfg!.MaxTurns);
    }

    [Fact]
    public async Task Task_agent_settings_persist_and_clear_max_turns()
    {
        await using var ctx = TestDb.NewContext();
        var tasks = new TaskRepository(ctx);
        await ctx.Tasks.AddAsync(new TaskEntity { Id = "T1", ListId = "L1", Title = "t" });
        await ctx.SaveChangesAsync();

        await tasks.UpdateAgentSettingsAsync("T1", model: null, systemPrompt: null, agentPath: null, maxTurns: 7);
        var t = await tasks.GetByIdAsync("T1");
        Assert.Equal(7, t!.MaxTurns);

        await tasks.UpdateAgentSettingsAsync("T1", null, null, null, maxTurns: null);
        var cleared = await tasks.GetByIdAsync("T1");
        Assert.Null(cleared!.MaxTurns);
    }
}

NOTE: replace TestDb.NewContext() with whatever the existing Data.Tests use (they may have a fixture or a static helper). Match the existing pattern exactly; do not invent a new harness. ListEntity requires only Id + Name (other fields have defaults) — verify against the existing tests.

  • Step 2: Run the test to verify it fails

Run: dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release --filter MaxTurnsRoundTripTests Expected: FAIL — UpdateAgentSettingsAsync has no maxTurns parameter (compile error), and SetConfigAsync does not copy MaxTurns onto existing rows.

  • Step 3: Update ListRepository.SetConfigAsync

In src/ClaudeDo.Data/Repositories/ListRepository.cs, in the else branch that updates an existing row (after existing.AgentPath = config.AgentPath;):

            existing.MaxTurns = config.MaxTurns;

(The Add branch already stores the full entity, so no change there.)

  • Step 4: Update TaskRepository.UpdateAgentSettingsAsync

Replace the method body in src/ClaudeDo.Data/Repositories/TaskRepository.cs:157-170 with:

    public async Task UpdateAgentSettingsAsync(
        string taskId,
        string? model,
        string? systemPrompt,
        string? agentPath,
        int? maxTurns = null,
        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)
                .SetProperty(t => t.MaxTurns, maxTurns), ct);
    }

maxTurns is placed before ct with a default so existing 4-arg callers still compile; we update those callers explicitly in Task 4.

  • Step 5: Run the test to verify it passes

Run: dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release --filter MaxTurnsRoundTripTests Expected: PASS (2 tests).

  • Step 6: Commit
git add src/ClaudeDo.Data/Repositories/ListRepository.cs src/ClaudeDo.Data/Repositories/TaskRepository.cs tests/ClaudeDo.Data.Tests/MaxTurnsRoundTripTests.cs
git commit -m "feat(data): persist max_turns in list and task repositories"

Task 3: Runner resolves MaxTurns task → list → global

Files:

  • Modify: src/ClaudeDo.Worker/Runner/TaskRunner.cs:388-394
  • Test: tests/ClaudeDo.Worker.Tests/Runner/MaxTurnsResolutionTests.cs

Context: BuildRunConfig currently returns MaxTurns: global.DefaultMaxTurns. ClaudeRunConfig.MaxTurns is int? and ClaudeArgsBuilder already emits --max-turns when > 0. Only the resolution line changes.

  • Step 1: Inspect the method signature

Open src/ClaudeDo.Worker/Runner/TaskRunner.cs around line 360-395 and confirm the names of the in-scope variables: the task entity (task), the list config (listConfig), and the app settings (global). If the config-building logic is not directly unit-testable (private method, lots of dependencies), extract a tiny pure static helper for just the MaxTurns precedence and test that:

    internal static int? ResolveMaxTurns(int? taskTurns, int? listTurns, int globalDefault)
        => taskTurns ?? listTurns ?? globalDefault;

Place it in TaskRunner as an internal static method.

  • Step 2: Write the failing test

Create tests/ClaudeDo.Worker.Tests/Runner/MaxTurnsResolutionTests.cs:

using ClaudeDo.Worker.Runner;
using Xunit;

namespace ClaudeDo.Worker.Tests.Runner;

public class MaxTurnsResolutionTests
{
    [Fact]
    public void Task_override_wins()
        => Assert.Equal(5, TaskRunner.ResolveMaxTurns(taskTurns: 5, listTurns: 20, globalDefault: 100));

    [Fact]
    public void List_override_used_when_no_task_override()
        => Assert.Equal(20, TaskRunner.ResolveMaxTurns(taskTurns: null, listTurns: 20, globalDefault: 100));

    [Fact]
    public void Falls_back_to_global_default()
        => Assert.Equal(100, TaskRunner.ResolveMaxTurns(taskTurns: null, listTurns: null, globalDefault: 100));
}
  • Step 3: Run the test to verify it fails

Run: dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter MaxTurnsResolutionTests Expected: FAIL — ResolveMaxTurns not defined.

  • Step 4: Add the helper and use it in BuildRunConfig

Add the ResolveMaxTurns helper from Step 1 to TaskRunner. Then change the MaxTurns: argument in the ClaudeRunConfig construction (currently MaxTurns: global.DefaultMaxTurns,) to:

            MaxTurns: ResolveMaxTurns(task.MaxTurns, listConfig?.MaxTurns, global.DefaultMaxTurns),
  • Step 5: Run the test to verify it passes

Run: dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter MaxTurnsResolutionTests Expected: PASS (3 tests).

  • Step 6: Build Worker

Run: dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release Expected: build succeeds.

  • Step 7: Commit
git add src/ClaudeDo.Worker/Runner/TaskRunner.cs tests/ClaudeDo.Worker.Tests/Runner/MaxTurnsResolutionTests.cs
git commit -m "feat(worker): resolve max-turns from task then list then global default"

Task 4: Thread MaxTurns through Worker transport (DTOs, Hub, MCP)

Files:

  • Modify: src/ClaudeDo.Worker/Hub/WorkerHub.cs:58-60, 334-368, 406-417

  • Modify: src/ClaudeDo.Worker/External/ConfigMcpTools.cs

  • Step 1: Extend the Worker-side DTO records

In src/ClaudeDo.Worker/Hub/WorkerHub.cs (lines 58-60), add a trailing nullable int? to each record (defaulted so existing positional usages elsewhere keep compiling):

public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
  • Step 2: Persist MaxTurns in UpdateListConfig

In WorkerHub.UpdateListConfig (lines 334-359): include MaxTurns in the "is everything empty?" delete check and in the SetConfigAsync call.

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

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

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

        await _broadcaster.ListUpdated(dto.ListId);
    }
  • Step 3: Return MaxTurns from GetListConfig

In WorkerHub.GetListConfig (line 367), change the return to include MaxTurns:

        return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns);
  • Step 4: Persist MaxTurns in UpdateTaskAgentSettings

In WorkerHub.UpdateTaskAgentSettings (lines 406-417), pass dto.MaxTurns to the repository (note the new maxTurns parameter sits before ct):

        await repo.UpdateAgentSettingsAsync(
            dto.TaskId,
            dto.Model.NullIfBlank(),
            dto.SystemPrompt.NullIfBlank(),
            dto.AgentPath.NullIfBlank(),
            dto.MaxTurns);
  • Step 5: Extend the MCP config tools

In src/ClaudeDo.Worker/External/ConfigMcpTools.cs:

Update the DTO (line 9):

public sealed record TaskConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns);

Update GetListConfig (line 29):

        return cfg is null ? null : new TaskConfigDto(cfg.Model, cfg.SystemPrompt, cfg.AgentPath, cfg.MaxTurns);

Update SetListConfig signature + body — add an int? maxTurns parameter (before CancellationToken), include it in the clear-check and the upsert:

    [McpServerTool, Description("Set a list's default model/system prompt/agent path/max turns. Passing model, systemPrompt, agentPath, and maxTurns all null clears the list config.")]
    public async Task SetListConfig(
        string listId, string? model, string? systemPrompt, string? agentPath, int? maxTurns, CancellationToken cancellationToken)
    {
        _ = await _lists.GetByIdAsync(listId, cancellationToken)
            ?? throw new InvalidOperationException($"List {listId} not found.");

        var m = model.NullIfBlank();
        var sp = systemPrompt.NullIfBlank();
        var ap = agentPath.NullIfBlank();

        if (m is null && sp is null && ap is null && maxTurns is null)
            await _lists.DeleteConfigAsync(listId, cancellationToken);
        else
            await _lists.SetConfigAsync(new ListConfigEntity
            {
                ListId = listId, Model = m, SystemPrompt = sp, AgentPath = ap, MaxTurns = maxTurns,
            }, cancellationToken);

        await _broadcaster.ListUpdated(listId);
    }

Update SetTaskConfig signature + body — add int? maxTurns (before CancellationToken) and pass it through:

    [McpServerTool, Description("Set per-task config overrides (model/system prompt/agent path/max turns). Pass null for any field to clear that override.")]
    public async Task SetTaskConfig(
        string taskId, string? model, string? systemPrompt, string? agentPath, int? maxTurns, CancellationToken cancellationToken)
    {
        _ = await _tasks.GetByIdAsync(taskId, cancellationToken)
            ?? throw new InvalidOperationException($"Task {taskId} not found.");

        await _tasks.UpdateAgentSettingsAsync(taskId, model.NullIfBlank(), systemPrompt.NullIfBlank(), agentPath.NullIfBlank(), maxTurns, cancellationToken);
        await _broadcaster.TaskUpdated(taskId);
    }
  • Step 6: Build Worker

Run: dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release Expected: build succeeds. (If any other caller constructed these DTOs positionally and now mismatches, fix it — the new params are defaulted, so this should be clean.)

  • Step 7: Commit
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Worker/External/ConfigMcpTools.cs
git commit -m "feat(worker): expose max-turns override over signalr and mcp config tools"

Task 5: Mirror DTO changes in the UI transport layer

Files:

  • Modify: src/ClaudeDo.Ui/Services/WorkerClient.cs:524-526
  • Modify: tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs (only if a signature/record change breaks it)

The UI keeps its own copies of UpdateListConfigDto, UpdateTaskAgentSettingsDto, ListConfigDto. They must mirror the Worker exactly or SignalR (de)serialization drops the field. IWorkerClient method signatures don't change (they take whole DTOs), so the interface and stub need no new members — but verify the stub still compiles.

  • Step 1: Extend the UI DTO records

In src/ClaudeDo.Ui/Services/WorkerClient.cs (lines 524-526):

public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
  • Step 2: Build the UI project

Run: dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release Expected: build succeeds (this pulls in Ui + Data).

  • Step 3: Build the UI test project

Run: dotnet build tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release Expected: build succeeds. StubWorkerClient returns null for GetListConfigAsync and ignores UpdateTaskAgentSettingsAsync(dto), so the defaulted record params keep it compiling. If it breaks, fix only the affected line.

  • Step 4: Commit
git add src/ClaudeDo.Ui/Services/WorkerClient.cs
git commit -m "feat(ui): mirror max-turns field on signalr config dtos"

Task 6: Localization keys (en + de parity)

Files:

  • Modify: locales/en.json
  • Modify: locales/de.json

Localization.Tests enforces key parity. Find the right nesting by searching for the existing sibling keys (modals.listSettings.model, details.modelLabel, vm.details.effectiveIfInherited).

  • Step 1: Add the new keys to locales/en.json

Add (place each next to its siblings; values shown):

  • under modals.listSettings: "maxTurns": "Max turns"

  • under details: "maxTurnsLabel": "Max turns", "systemPromptPrepended": "Prepended automatically:"

  • a new/extended settings.inherit group (or wherever shared UI strings live — match existing convention):

    • "inheritedFromList": "inherited · List"
    • "inheritedFromGlobal": "inherited · Global"
    • "overrideBadge": "override"
    • "resetToInherited": "Reset to inherited"
  • Step 2: Add the same keys to locales/de.json

Same key paths, German values:

  • modals.listSettings.maxTurns: "Max. Turns"

  • details.maxTurnsLabel: "Max. Turns"

  • details.systemPromptPrepended: "Wird automatisch vorangestellt:"

  • settings.inherit.inheritedFromList: "geerbt · Liste"

  • settings.inherit.inheritedFromGlobal: "geerbt · Global"

  • settings.inherit.overrideBadge: "überschrieben"

  • settings.inherit.resetToInherited: "Auf geerbt zurücksetzen"

  • Step 3: Remove the now-unused effectiveIfInherited keys

Remove vm.details.effectiveIfInherited from BOTH en.json and de.json (it is replaced by the badge approach in Tasks 8-9; this plan deletes its only usages there). Removing from both keeps parity.

  • Step 4: Run the localization tests

Run: dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release Expected: PASS (en/de key parity holds).

  • Step 5: Commit
git add locales/en.json locales/de.json
git commit -m "feat(i18n): add inherited-marker, turns, and prepended-prompt strings"

Task 7: InheritanceResolver helper (pure, unit-tested)

Files:

  • Create: src/ClaudeDo.Ui/Services/InheritanceResolver.cs

  • Test: tests/ClaudeDo.Ui.Tests/InheritanceResolverTests.cs

  • Step 1: Write the failing test

Create tests/ClaudeDo.Ui.Tests/InheritanceResolverTests.cs:

using ClaudeDo.Ui.Services;
using Xunit;

namespace ClaudeDo.Ui.Tests;

public class InheritanceResolverTests
{
    [Fact]
    public void Task_value_is_an_override()
    {
        var (value, source) = InheritanceResolver.Resolve("opus", "sonnet", "haiku");
        Assert.Equal("opus", value);
        Assert.Equal(InheritSource.Override, source);
    }

    [Fact]
    public void Falls_through_to_list()
    {
        var (value, source) = InheritanceResolver.Resolve(null, "sonnet", "haiku");
        Assert.Equal("sonnet", value);
        Assert.Equal(InheritSource.List, source);
    }

    [Fact]
    public void Falls_through_to_global()
    {
        var (value, source) = InheritanceResolver.Resolve("  ", null, "haiku");
        Assert.Equal("haiku", value);
        Assert.Equal(InheritSource.Global, source);
    }

    [Fact]
    public void List_scope_treats_list_value_as_override()
    {
        var (value, source) = InheritanceResolver.ResolveList("sonnet", "haiku");
        Assert.Equal("sonnet", value);
        Assert.Equal(InheritSource.Override, source);

        var (value2, source2) = InheritanceResolver.ResolveList(null, "haiku");
        Assert.Equal("haiku", value2);
        Assert.Equal(InheritSource.Global, source2);
    }
}
  • Step 2: Run the test to verify it fails

Run: dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter InheritanceResolverTests Expected: FAIL — type does not exist.

  • Step 3: Implement the helper

Create src/ClaudeDo.Ui/Services/InheritanceResolver.cs:

namespace ClaudeDo.Ui.Services;

public enum InheritSource { Override, List, Global }

public static class InheritanceResolver
{
    // Task-scope fields: task -> list -> global.
    public static (string Value, InheritSource Source) Resolve(
        string? taskValue, string? listValue, string? globalValue)
    {
        if (!string.IsNullOrWhiteSpace(taskValue)) return (taskValue!, InheritSource.Override);
        if (!string.IsNullOrWhiteSpace(listValue)) return (listValue!, InheritSource.List);
        return (globalValue ?? "", InheritSource.Global);
    }

    // List-scope fields: list -> global (lists have no tier above them).
    public static (string Value, InheritSource Source) ResolveList(
        string? listValue, string? globalValue)
    {
        if (!string.IsNullOrWhiteSpace(listValue)) return (listValue!, InheritSource.Override);
        return (globalValue ?? "", InheritSource.Global);
    }
}
  • Step 4: Run the test to verify it passes

Run: dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter InheritanceResolverTests Expected: PASS (4 tests).

  • Step 5: Commit
git add src/ClaudeDo.Ui/Services/InheritanceResolver.cs tests/ClaudeDo.Ui.Tests/InheritanceResolverTests.cs
git commit -m "feat(ui): add inheritance resolver returning value and source"

Task 8: InheritedBadge reusable control

Files:

  • Create: src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml
  • Create: src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml.cs

A tiny UserControl rendering a single badge whose text comes from a bound string. The consuming VM supplies the already-localized badge text (e.g. "inherited · List" / "override") and a bool for muted styling. Keeping localization in the VM avoids Loc lookups inside the control. No ICommand lives in the control — reset buttons are placed by each consumer (Tasks 9-10).

  • Step 1: Create the control XAML

src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml:

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="ClaudeDo.Ui.Views.Controls.InheritedBadge"
             x:Name="Root">
  <Border Background="{DynamicResource SubtleFillBrush}"
          CornerRadius="4" Padding="6,1"
          IsVisible="{Binding #Root.BadgeText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
          VerticalAlignment="Center" HorizontalAlignment="Left">
    <TextBlock Classes="meta" FontSize="11" Opacity="0.75"
               Text="{Binding #Root.BadgeText}"/>
  </Border>
</UserControl>

If SubtleFillBrush is not a defined token, use an existing subtle background brush from Design/Tokens.axaml (open it and pick the closest — e.g. a hover/overlay brush). The user will do the final visual pass.

  • Step 2: Create the code-behind with a BadgeText StyledProperty

src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml.cs:

using Avalonia;
using Avalonia.Controls;

namespace ClaudeDo.Ui.Views.Controls;

public partial class InheritedBadge : UserControl
{
    public static readonly StyledProperty<string?> BadgeTextProperty =
        AvaloniaProperty.Register<InheritedBadge, string?>(nameof(BadgeText));

    public string? BadgeText
    {
        get => GetValue(BadgeTextProperty);
        set => SetValue(BadgeTextProperty, value);
    }

    public InheritedBadge() => InitializeComponent();
}
  • Step 3: Build the UI project

Run: dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release Expected: build succeeds (control compiles, even though nothing consumes it yet).

  • Step 4: Commit
git add src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml.cs
git commit -m "feat(ui): add reusable inherited-source badge control"

Task 9: List settings modal — Turns field + inherited markers

Files:

  • Modify: src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs
  • Modify: src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml

Lists inherit only from Global. The ViewModel loads the global defaults (via GetAppSettingsAsync) to render the muted resolved value + inherited · Global badge. For Model we drop the (default) sentinel row and use SelectedItem == null to mean "inherited", with PlaceholderText showing the resolved value. Turns uses a NumericUpDown whose Value is decimal? (null = inherit) with a Watermark showing the global default.

  • Step 1: Add fields + load logic to the ViewModel

In ListSettingsModalViewModel.cs:

  1. Add a using ClaudeDo.Ui.Services; if not present (for InheritanceResolver / InheritSource).
  2. Change the Model collection to not include the sentinel:
    public ObservableCollection<string> ModelOptions { get; } = new(ModelRegistry.Aliases);
  1. Change SelectedModel to nullable (null = inherit) and add the badge + Turns properties:
    [ObservableProperty] private string? _selectedModel;          // null = inherit from global
    [ObservableProperty] private decimal? _maxTurns;              // null = inherit from global
    [ObservableProperty] private string _modelInheritedHint = ""; // muted resolved value, e.g. "sonnet"
    [ObservableProperty] private string _modelBadge = "";         // localized badge text
    [ObservableProperty] private string _turnsInheritedHint = ""; // muted resolved value, e.g. "100"
    [ObservableProperty] private string _turnsBadge = "";
    [ObservableProperty] private string _agentBadge = "";
    [ObservableProperty] private string _agentInheritedHint = "";
  1. Add badge recompute helpers and wire them to property changes:
    partial void OnSelectedModelChanged(string? value) => RecomputeModelBadge();
    partial void OnMaxTurnsChanged(decimal? value) => RecomputeTurnsBadge();
    partial void OnSelectedAgentChanged(AgentInfo? value) => RecomputeAgentBadge();

    private string _globalModel = ModelRegistry.DefaultAlias;
    private int _globalMaxTurns = 100;

    private void RecomputeModelBadge()
    {
        var overridden = !string.IsNullOrWhiteSpace(SelectedModel);
        ModelInheritedHint = _globalModel;
        ModelBadge = overridden ? Loc.T("settings.inherit.overrideBadge") : Loc.T("settings.inherit.inheritedFromGlobal");
    }

    private void RecomputeTurnsBadge()
    {
        var overridden = MaxTurns is not null;
        TurnsInheritedHint = _globalMaxTurns.ToString();
        TurnsBadge = overridden ? Loc.T("settings.inherit.overrideBadge") : Loc.T("settings.inherit.inheritedFromGlobal");
    }

    private void RecomputeAgentBadge()
    {
        var overridden = SelectedAgent is not null && !string.IsNullOrWhiteSpace(SelectedAgent.Path);
        // Agent has no global default; inherited means "(none)".
        AgentInheritedHint = "(none)";
        AgentBadge = overridden ? Loc.T("settings.inherit.overrideBadge") : Loc.T("settings.inherit.inheritedFromGlobal");
    }
  1. In LoadAsync, load globals and seed the new fields. After the GetListConfigAsync block, replace the Model/Agent seeding with the null-sentinel approach and add MaxTurns + globals:
        var app = await _worker.GetAppSettingsAsync();
        _globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
        _globalMaxTurns = app?.DefaultMaxTurns ?? 100;

        SelectedModel = string.IsNullOrWhiteSpace(config?.Model) ? null : config!.Model!;
        MaxTurns = config?.MaxTurns is int mt ? mt : (decimal?)null;
        SystemPrompt = config?.SystemPrompt ?? "";
        SelectedAgent = string.IsNullOrWhiteSpace(config?.AgentPath)
            ? Agents[0]
            : (Agents.FirstOrDefault(a => a.Path == config!.AgentPath) ?? Agents[0]);

        RecomputeModelBadge();
        RecomputeTurnsBadge();
        RecomputeAgentBadge();

(Remove the old line that set SelectedModel = ... ListDefaultSentinel ....)

  1. In SaveAsync, compute the override values and include MaxTurns:
        var model = string.IsNullOrWhiteSpace(SelectedModel) ? null : SelectedModel;
        var sp = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt;
        var ap = SelectedAgent is null || string.IsNullOrWhiteSpace(SelectedAgent.Path) ? null : SelectedAgent.Path;
        var turns = MaxTurns is decimal d ? (int?)d : null;

        await _worker.UpdateListAsync(new UpdateListDto(
            ListId,
            string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name,
            string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir,
            DefaultCommitType));

        await _worker.UpdateListConfigAsync(new UpdateListConfigDto(
            ListId, model, sp, ap, turns));
  1. Add reset commands and update ResetAgentSettings:
    [RelayCommand] private void ResetModel() => SelectedModel = null;
    [RelayCommand] private void ResetTurns() => MaxTurns = null;
    [RelayCommand] private void ResetAgent() => SelectedAgent = Agents.Count > 0 ? Agents[0] : null;

And in the existing ResetAgentSettings, replace SelectedModel = ModelRegistry.ListDefaultSentinel; with SelectedModel = null; and add MaxTurns = null;.

  • Step 2: Update the modal XAML — Model field

In ListSettingsModalView.axaml, add xmlns:ctl="using:ClaudeDo.Ui.Views.Controls" to the root if not already present (it is). Replace the Model StackPanel (lines ~76-81) with a label-row that carries the badge + reset, and a ComboBox using null-selection + placeholder:

              <StackPanel Spacing="4">
                <Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
                  <TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr modals.listSettings.model}" VerticalAlignment="Center"/>
                  <ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding ModelBadge}"/>
                  <Button Grid.Column="3" Classes="btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
                          Command="{Binding ResetModelCommand}" Padding="6,1"/>
                </Grid>
                <ComboBox ItemsSource="{Binding ModelOptions}"
                          SelectedItem="{Binding SelectedModel, Mode=TwoWay}"
                          PlaceholderText="{Binding ModelInheritedHint}"
                          HorizontalAlignment="Left" MinWidth="160" />
              </StackPanel>
  • Step 3: Update the modal XAML — add the Turns field

Add a new StackPanel inside the AGENT section's inner StackPanel (after the Model field, before System prompt):

              <StackPanel Spacing="4">
                <Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
                  <TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr modals.listSettings.maxTurns}" VerticalAlignment="Center"/>
                  <ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding TurnsBadge}"/>
                  <Button Grid.Column="3" Classes="btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
                          Command="{Binding ResetTurnsCommand}" Padding="6,1"/>
                </Grid>
                <NumericUpDown Value="{Binding MaxTurns, Mode=TwoWay}"
                               Watermark="{Binding TurnsInheritedHint}"
                               Minimum="1" Maximum="200" Increment="1" FormatString="0"
                               HorizontalAlignment="Left" Width="160"/>
              </StackPanel>
  • Step 4: Update the modal XAML — Agent field badge + reset

Wrap the existing Agent field-label (line ~91) in the same label-row pattern so it shows AgentBadge and a reset button bound to ResetAgentCommand. Keep the existing ComboBox + browse button + path TextBlock unchanged below it.

  • Step 5: Build the UI project

Run: dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release Expected: build succeeds. Fix any binding/compile errors (compiled bindings require the VM properties to exist — they were added in Step 1).

  • Step 6: Commit
git add src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml
git commit -m "feat(ui): show inherited markers and max-turns override in list settings"

Task 10: Task overrides flyout — Turns field + inherited markers

Files:

  • Modify: src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
  • Modify: src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml:90-122

Task scope falls through task → list → global. Model resolves to global default; Agent has no global tier (inherited = list value or "(none)"); Turns resolves to list value or global default. Reuse InheritanceResolver.Resolve. Drop the (inherit) sentinel from the Model dropdown and the Agent dropdown's first row; null selection = inherited.

  • Step 1: Replace the effective-hint fields with badge fields in the ViewModel

In DetailsIslandViewModel.cs:

  1. Remove EffectiveModelLabel / EffectiveAgentLabel computed props and the OnEffective*HintChanged partials (lines ~155-159), and the OnPropertyChanged(nameof(EffectiveModelLabel)) calls at lines ~141-142 and ~291-292.
  2. Change the Model options to drop the sentinel:
    public System.Collections.ObjectModel.ObservableCollection<string> TaskModelOptions { get; } = new(ModelRegistry.Aliases);
  1. Change TaskModelSelection to nullable and add new observable fields:
    [ObservableProperty] private string? _taskModelSelection;     // null = inherit
    [ObservableProperty] private decimal? _taskMaxTurns;          // null = inherit
    [ObservableProperty] private string _modelBadge = "";
    [ObservableProperty] private string _modelInheritedHint = "";
    [ObservableProperty] private string _turnsBadge = "";
    [ObservableProperty] private string _turnsInheritedHint = "";
    [ObservableProperty] private string _agentBadge = "";
    // keep existing _taskSystemPrompt; EffectiveSystemPromptHint stays as the prepended-prompt hint

Keep EffectiveSystemPromptHint (used as the read-only "prepended automatically" hint). Remove EffectiveModelHint and EffectiveAgentHint if nothing else references them after this task. 4. Track globals and recompute badges:

    private string _globalModel = ModelRegistry.DefaultAlias;
    private int _globalMaxTurns = 100;

    partial void OnTaskModelSelectionChanged(string? value) => RecomputeModelBadge();
    partial void OnTaskMaxTurnsChanged(decimal? value) => RecomputeTurnsBadge();
    partial void OnTaskSelectedAgentChanged(AgentInfo? value) => RecomputeAgentBadge();

    private string? _listModel, _listAgentName;
    private int? _listMaxTurns;

    private void RecomputeModelBadge()
    {
        var (value, source) = InheritanceResolver.Resolve(TaskModelSelection, _listModel, _globalModel);
        ModelInheritedHint = value;
        ModelBadge = BadgeFor(source, taskSet: !string.IsNullOrWhiteSpace(TaskModelSelection));
    }

    private void RecomputeTurnsBadge()
    {
        var (value, source) = InheritanceResolver.Resolve(
            TaskMaxTurns?.ToString(), _listMaxTurns?.ToString(), _globalMaxTurns.ToString());
        TurnsInheritedHint = value;
        TurnsBadge = BadgeFor(source, taskSet: TaskMaxTurns is not null);
    }

    private void RecomputeAgentBadge()
    {
        var taskSet = TaskSelectedAgent is not null && !string.IsNullOrWhiteSpace(TaskSelectedAgent.Path);
        var (_, source) = InheritanceResolver.Resolve(
            taskSet ? TaskSelectedAgent!.Path : null, _listAgentName, null);
        AgentBadge = BadgeFor(source, taskSet);
    }

    private static string BadgeFor(InheritSource source, bool taskSet) => taskSet
        ? Loc.T("settings.inherit.overrideBadge")
        : source == InheritSource.List
            ? Loc.T("settings.inherit.inheritedFromList")
            : Loc.T("settings.inherit.inheritedFromGlobal");
  • Step 2: Update LoadAgentSettingsAsync to seed the new fields

Replace the seeding block (lines ~504-514) with:

            TaskModelSelection = string.IsNullOrWhiteSpace(entity.Model) ? null : entity.Model!;
            TaskMaxTurns = entity.MaxTurns is int tmt ? tmt : (decimal?)null;
            TaskSystemPrompt = entity.SystemPrompt ?? "";
            TaskSelectedAgent = string.IsNullOrWhiteSpace(entity.AgentPath)
                ? TaskAgentOptions[0]
                : (TaskAgentOptions.FirstOrDefault(a => a.Path == entity.AgentPath) ?? TaskAgentOptions[0]);

            var listCfg = await _worker.GetListConfigAsync(entity.ListId);
            var app = await _worker.GetAppSettingsAsync();
            _globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
            _globalMaxTurns = app?.DefaultMaxTurns ?? 100;
            _listModel = listCfg?.Model;
            _listMaxTurns = listCfg?.MaxTurns;
            _listAgentName = string.IsNullOrWhiteSpace(listCfg?.AgentPath)
                ? null : System.IO.Path.GetFileName(listCfg!.AgentPath!);

            EffectiveSystemPromptHint = string.IsNullOrWhiteSpace(listCfg?.SystemPrompt) ? "" : listCfg!.SystemPrompt!;

            RecomputeModelBadge();
            RecomputeTurnsBadge();
            RecomputeAgentBadge();

Also drop the (inherit) sentinel row added to TaskAgentOptions (line ~500): replace TaskAgentOptions.Add(new AgentInfo(ModelRegistry.TaskInheritSentinel, "", "")); with a "(none)"/inherit placeholder row (path empty) so TaskAgentOptions[0] still means "inherited": TaskAgentOptions.Add(new AgentInfo("(inherited)", "", ""));

  • Step 3: Update the save + clear paths

In the agent-save method (line ~481), include MaxTurns:

            var model = string.IsNullOrWhiteSpace(TaskModelSelection) ? null : TaskModelSelection;
            var sp = string.IsNullOrWhiteSpace(TaskSystemPrompt) ? null : TaskSystemPrompt;
            var ap = TaskSelectedAgent is null || string.IsNullOrWhiteSpace(TaskSelectedAgent.Path)
                ? null : TaskSelectedAgent.Path;
            var turns = TaskMaxTurns is decimal d ? (int?)d : null;

            await _worker.UpdateTaskAgentSettingsAsync(
                new ClaudeDo.Ui.Services.UpdateTaskAgentSettingsDto(Task.Id, model, sp, ap, turns));

In the Bind(null) reset block (lines ~583-596), set the new fields:

                TaskModelSelection = null;
                TaskMaxTurns = null;
                TaskSystemPrompt = "";
                TaskSelectedAgent = null;

and remove the now-deleted EffectiveModelHint/EffectiveAgentHint assignments; keep EffectiveSystemPromptHint = "";.

  • Step 4: Add reset commands
    [RelayCommand] private void ResetTaskModel() => TaskModelSelection = null;
    [RelayCommand] private void ResetTaskTurns() => TaskMaxTurns = null;
    [RelayCommand] private void ResetTaskAgent() => TaskSelectedAgent = TaskAgentOptions.Count > 0 ? TaskAgentOptions[0] : null;
  • Step 5: Update the flyout XAML

In DetailsIslandView.axaml, add xmlns:ctl="using:ClaudeDo.Ui.Views.Controls" to the root if absent. Replace the Model block (lines ~90-98) with the badge+reset label row + null-select ComboBox:

                <StackPanel Spacing="2">
                  <Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
                    <TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.modelLabel}" VerticalAlignment="Center"/>
                    <ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding ModelBadge}"/>
                    <Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
                            Command="{Binding ResetTaskModelCommand}"/>
                  </Grid>
                  <ComboBox ItemsSource="{Binding TaskModelOptions}"
                            SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}"
                            PlaceholderText="{Binding ModelInheritedHint}"
                            HorizontalAlignment="Stretch"/>
                </StackPanel>

                <StackPanel Spacing="2">
                  <Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
                    <TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.maxTurnsLabel}" VerticalAlignment="Center"/>
                    <ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding TurnsBadge}"/>
                    <Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
                            Command="{Binding ResetTaskTurnsCommand}"/>
                  </Grid>
                  <NumericUpDown Value="{Binding TaskMaxTurns, Mode=TwoWay}"
                                 Watermark="{Binding TurnsInheritedHint}"
                                 Minimum="1" Maximum="200" Increment="1" FormatString="0"
                                 HorizontalAlignment="Stretch"/>
                </StackPanel>

For the System prompt block (lines ~100-105), keep the TextBox but change the placeholder hint label to use the new "prepended automatically" key by adding a small caption above/below it:

                <StackPanel Spacing="2">
                  <TextBlock Classes="field-label" Text="{loc:Tr details.systemPromptLabel}"/>
                  <TextBox Text="{Binding TaskSystemPrompt, Mode=TwoWay}"
                           AcceptsReturn="True" TextWrapping="Wrap" MinHeight="70"/>
                  <TextBlock Classes="meta" Opacity="0.6"
                             Text="{loc:Tr details.systemPromptPrepended}"
                             IsVisible="{Binding EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
                  <TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
                             Text="{Binding EffectiveSystemPromptHint}"
                             IsVisible="{Binding EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
                </StackPanel>

For the Agent block (lines ~107-121), wrap the field-label in the badge+reset row (badge bound to AgentBadge, reset to ResetTaskAgentCommand); remove the old EffectiveAgentLabel <TextBlock> at lines ~118-120; keep the ComboBox.

  • Step 6: Build the UI project

Run: dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release Expected: build succeeds. Resolve any leftover references to the removed Effective*Label/Effective*Hint members.

  • Step 7: Run the full UI test suite

Run: dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release Expected: PASS. If StubWorkerClient.GetAppSettingsAsync returning null causes a VM test NRE, the VM already null-coalesces (app?.DefaultModel ?? ...) — but verify any DetailsIsland VM test still passes; adjust the stub to return a minimal AppSettingsDto if a test needs non-null globals.

  • Step 8: Commit
git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
git commit -m "feat(ui): show inherited markers and max-turns override in task flyout"

Task 11: Full build + test sweep, docs, visual-verification flag

Files:

  • Modify: src/ClaudeDo.Ui/CLAUDE.md (one-line note on the new flyout/modal fields), src/ClaudeDo.Data/CLAUDE.md (note max_turns on ListConfig/TaskEntity)

  • Step 1: Build everything that matters

Run, in order:

dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release

Expected: both succeed.

  • Step 2: Run the test suites

Run:

dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release

Expected: all PASS.

  • Step 3: Update the two CLAUDE.md notes

In src/ClaudeDo.Data/CLAUDE.md, update the ListConfigEntity and TaskEntity lines to mention MaxTurns (nullable override). In src/ClaudeDo.Ui/CLAUDE.md, update the ListSettingsModalView and DetailsIslandView descriptions to mention the Max-turns field and source-aware inherited markers.

  • Step 4: Commit docs
git add src/ClaudeDo.Data/CLAUDE.md src/ClaudeDo.Ui/CLAUDE.md
git commit -m "docs: note max-turns override and inherited markers in module docs"
  • Step 5: Flag the visual-verification gap (do NOT claim the UI works)

The badge styling, muted placeholder appearance, NumericUpDown watermark, and reset-button layout in both the List settings modal and the task overrides flyout require a human visual pass — they cannot be asserted programmatically. Report to the user: build + automated tests pass; the visual rendering of the inherited markers, placeholders, and reset buttons needs manual confirmation in the running app (open a list's settings and a task's ⚙ flyout, toggle override vs inherited for Model / Turns / Agent).


Self-Review notes

  • Spec coverage: marker shows resolved value muted (placeholder/watermark) + source-aware badge (Tasks 9-10 via Task 7 resolver) ✓; override semantics with reset (reset commands, Tasks 9-10) ✓; Turns at list + task levels (Tasks 1-5, 9-10) ✓; SystemPrompt stays plain + "prepended automatically" hint (Task 10 Step 5) ✓; MCP parity (Task 4) ✓; localization parity + retire old key (Task 6) ✓; test-fake sync (Task 5) ✓.
  • Naming consistency: badge VM props named ModelBadge / TurnsBadge / AgentBadge; hints *InheritedHint; reset commands Reset*Command; resolver Resolve / ResolveList returning (string Value, InheritSource Source); control property BadgeText. These names are used identically across tasks.
  • Known risk: AppSettingsDto already carries DefaultModel and DefaultMaxTurns (confirmed) — UI globals load works. NumericUpDown.Value is decimal?, hence the decimal? VM properties with int? conversion at save.