Files
ClaudeDo/docs/superpowers/plans/2026-10-01-unit-branch-1-naming-base-branch.md

46 KiB

Unit-Branch Teil 1: Branch-Naming + Basis-Branch pro Liste — 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: Task-Branches heißen <typ>/<nummer>-<slug> statt claudedo/{nr}-{slug}, Firmen-Repos werden über ein konfigurierbares Remote-Muster erkannt, und jede Liste kann einen Basis-Branch (z.B. dev) festlegen, der Basis neuer Worktrees und Default-Merge-Ziel ist.

Architecture: Erkennung = statischer Helper CompanyRepo über git remote get-url origin + WorkerConfig.CompanyRemotePattern. Naming bleibt in WorktreeManager (NameFor + BranchTypeFor). list_config.base_branch ist eine neue nullable Spalte, tri-state durch alle Writer gereicht; WorktreeManager.ResolveBaseCommitAsync und TaskMergeService.ResolveTargetAsync lesen sie.

Tech Stack: .NET 8, EF Core (SQLite), xUnit mit echtem git, Avalonia 12.

Spec: docs/superpowers/specs/2026-10-01-unit-branch-draft-pr-design.md (Abschnitt 1 + Basis-Branch). Teil 2 (Unit-Branch/Children) und Teil 3 (Firmen-Modus: Fetch, Push, Draft-PR) folgen als eigene Pläne.

Abweichung von der Spec (bewusst): Der Self-Heal in WorktreeManager.CreateAsync bekommt keinen zusätzlichen DB-Ownership-Guard. Er feuert nur, wenn auch der Id-suffixte Name schon existiert — und der enthält die Task-Id, gehört also per Konstruktion diesem Task. Ein fremder Branch mit dem Klarnamen wird schon vom Kollisionscheck auf das Suffix umgelenkt. Nötig ist nur, dass der Kollisionscheck den vollen neuen Branchnamen prüft (Task 3).

Konventionen für jede Task:

  • Build: dotnet build ClaudeDo.slnx -c Release (nie Debug — ein laufender Worker lockt es).
  • Tests: dotnet test <projekt> -c Release --no-build --filter "<filter>" — nie die volle Suite.
  • Commit: git add <pfade> explizit, dann git commit -m "..." -- <pfade> (geteilter Checkout, nie git add -A). Kein Co-Authored-Trailer.

Dateiübersicht

Datei Änderung
src/ClaudeDo.Worker/Config/WorkerConfig.cs + CompanyRemotePattern (company_remote_pattern, Default /_git/)
src/ClaudeDo.Data/Git/GitService.cs + GetRemoteUrlAsync
src/ClaudeDo.Worker/Git/CompanyRepo.cs neu: Matches, IsCompanyAsync
src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs ToSlug löst Umlaute auf
src/ClaudeDo.Worker/Runner/WorktreeManager.cs NameFor(task, ticketId, isCompany), BranchTypeFor, neuer Branchname, Ticket-Auflösung, Basis-Branch
src/ClaudeDo.Worker/Lifecycle/UntrackedWorktreeRecovery.cs Pfad- statt Präfix-Filter
src/ClaudeDo.Data/Models/ListConfigEntity.cs, Configuration/ListConfigEntityConfiguration.cs, Repositories/ListRepository.cs, Migration + BaseBranch
src/ClaudeDo.Data/Wire.cs UpdateListConfigDto / ListConfigDto + BaseBranch
src/ClaudeDo.Worker/Hub/WorkerHub.cs UpdateListConfig / GetListConfig
src/ClaudeDo.Worker/External/ConfigMcpTools.cs erhält BaseBranch und PowerUseAllowed (Bestandsbug), get_list_config liefert baseBranch
src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs ResolveTargetAsync, leeres Ziel zentral in MergeAsync aufgelöst
src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs, ViewModels/Modals/ListSettingsModalViewModel.cs, Views/Modals/ListSettingsModalView.axaml, locales/*.json Basis-Branch-Feld
src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs echte Branchnamen in Dialogen, kein ← main
src/ClaudeDo.Worker/CLAUDE.md, src/ClaudeDo.Data/CLAUDE.md Doku

Task 1: Firmen-Repo-Erkennung

Files:

  • Modify: src/ClaudeDo.Worker/Config/WorkerConfig.cs (nach TicketApiBaseUrl, ~Zeile 71)

  • Modify: src/ClaudeDo.Data/Git/GitService.cs (neben ListLocalBranchesAsync, ~Zeile 404)

  • Create: src/ClaudeDo.Worker/Git/CompanyRepo.cs

  • Test: tests/ClaudeDo.Worker.Tests/Git/CompanyRepoTests.cs

  • Step 1: Failing test schreiben

using ClaudeDo.Data.Git;
using ClaudeDo.Worker.Git;
using ClaudeDo.Worker.Tests.Infrastructure;

namespace ClaudeDo.Worker.Tests.Git;

public sealed class CompanyRepoTests : IDisposable
{
    private readonly GitRepoFixture _repo = new();
    public void Dispose() => _repo.Dispose();

    [Theory]
    [InlineData("https://srv.local/Coll/Proj/_git/Repo", "/_git/", true)]
    [InlineData("https://mika@srv.local/tfs/Coll/Proj/_git/Mein%20Repo.git", "/_git/", true)]
    [InlineData("ssh://git@git.kuns.dev:22222/kuns/ClaudeDo.git", "/_git/", false)]
    [InlineData(null, "/_git/", false)]
    [InlineData("https://srv.local/Coll/Proj/_git/Repo", "", false)]
    [InlineData("https://GITHUB.com/acme/repo.git", "github.com/acme", true)]
    public void Matches(string? url, string pattern, bool expected)
        => Assert.Equal(expected, CompanyRepo.Matches(url, pattern));

    [Fact]
    public async Task IsCompanyAsync_reads_origin()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;
        var git = new GitService();
        Assert.False(await CompanyRepo.IsCompanyAsync(git, _repo.RepoDir, "/_git/", CancellationToken.None));

        GitRepoFixture.RunGit(_repo.RepoDir, "remote", "add", "origin", "https://srv.local/Coll/Proj/_git/Repo");

        Assert.True(await CompanyRepo.IsCompanyAsync(git, _repo.RepoDir, "/_git/", CancellationToken.None));
        Assert.Equal("https://srv.local/Coll/Proj/_git/Repo", await git.GetRemoteUrlAsync(_repo.RepoDir));
    }
}
  • Step 2: Test laufen lassen — muss am Compiler scheitern

Run: dotnet build ClaudeDo.slnx -c Release Expected: FAIL, CompanyRepo / GetRemoteUrlAsync existieren nicht.

  • Step 3: Implementieren

WorkerConfig.cs, direkt nach der TicketApiBaseUrl-Property:

    /// <summary>Teilstring der <c>origin</c>-URL, an dem ein Firmen-Repo erkannt wird. Nur per Hand editierbar.</summary>
    [JsonPropertyName("company_remote_pattern")]
    public string CompanyRemotePattern { get; set; } = "/_git/";

GitService.cs, neben ListLocalBranchesAsync:

    /// <summary>URL des Remotes <paramref name="remote"/>, oder null wenn es keins gibt.</summary>
    public async Task<string?> GetRemoteUrlAsync(string repoDir, string remote = "origin", CancellationToken ct = default)
    {
        var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["remote", "get-url", remote], ct);
        return exitCode == 0 && !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : null;
    }

src/ClaudeDo.Worker/Git/CompanyRepo.cs:

using ClaudeDo.Data.Git;

namespace ClaudeDo.Worker.Git;

/// <summary>Erkennt Firmen-Repos am <c>origin</c>-Remote; das Muster kommt aus <c>WorkerConfig.CompanyRemotePattern</c>.</summary>
public static class CompanyRepo
{
    /// <summary>True wenn <paramref name="remoteUrl"/> das (nicht-leere) Muster enthält, ohne Groß-/Kleinschreibung.</summary>
    public static bool Matches(string? remoteUrl, string? pattern) =>
        !string.IsNullOrWhiteSpace(remoteUrl) && !string.IsNullOrWhiteSpace(pattern)
        && remoteUrl.Contains(pattern, StringComparison.OrdinalIgnoreCase);

    /// <summary>Liest <c>origin</c> live (kein Cache — ein Remote kann sich jederzeit ändern).</summary>
    public static async Task<bool> IsCompanyAsync(GitService git, string workingDir, string? pattern, CancellationToken ct) =>
        Matches(await git.GetRemoteUrlAsync(workingDir, "origin", ct), pattern);
}
  • Step 4: Build + Test

Run: dotnet build ClaudeDo.slnx -c Release → 0 Fehler. Run: dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CompanyRepoTests" Expected: PASS (7 Tests).

  • Step 5: Commit
git add src/ClaudeDo.Worker/Config/WorkerConfig.cs src/ClaudeDo.Data/Git/GitService.cs src/ClaudeDo.Worker/Git/CompanyRepo.cs tests/ClaudeDo.Worker.Tests/Git/CompanyRepoTests.cs
git commit -m "feat(worker): detect company repos by origin remote pattern" -- src/ClaudeDo.Worker/Config/WorkerConfig.cs src/ClaudeDo.Data/Git/GitService.cs src/ClaudeDo.Worker/Git/CompanyRepo.cs tests/ClaudeDo.Worker.Tests/Git/CompanyRepoTests.cs

Task 2: Naming-Bausteine (Typ, Nummer, Umlaute)

Files:

  • Modify: src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs:49-60 (ToSlug)

  • Modify: src/ClaudeDo.Worker/Runner/WorktreeManager.cs:41-47 (NameFor) + neues BranchTypeFor

  • Test: tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs:82-89 (Theory ersetzen), tests/ClaudeDo.Worker.Tests/Runner/CommitMessageBuilderTests.cs

  • Step 1: Tests anpassen/ergänzen

In WorktreeManagerTests.cs die Theory NameFor_NumberPlusCappedTitleSlug (Zeilen 82-89) ersetzen durch:

    [Theory]
    [InlineData("Fix: Löschen   des Tasks!", null, false, "7-fix-loeschen-des-tasks")]
    [InlineData("äöü", null, false, "7-aeoeue")]
    [InlineData("a very long title that goes well beyond the forty char cap", null, false, "7-a-very-long-title-that-goes-well-beyond")]
    [InlineData("test task", 412, false, "412-test-task")]
    [InlineData("test task", 412, true, "412-test-task")]
    [InlineData("test task", null, true, "test-task")]
    [InlineData("!!!", null, true, "7")]
    public void NameFor_NumberRules(string title, int? ticketId, bool isCompany, string expected)
    {
        var task = new TaskEntity { Id = "x", ListId = "y", Title = title, Number = 7, CreatedAt = DateTime.UtcNow };
        Assert.Equal(expected, WorktreeManager.NameFor(task, ticketId, isCompany));
    }

    [Theory]
    [InlineData("feat", "feature")]
    [InlineData("fix", "fix")]
    [InlineData("refactor", "refactor")]
    [InlineData("docs", "docs")]
    [InlineData("test", "test")]
    [InlineData("chore", "chore")]
    [InlineData("ci", "chore")]
    [InlineData("perf", "chore")]
    [InlineData(" FEAT ", "feature")]
    [InlineData("", "chore")]
    public void BranchTypeFor_maps_commit_types(string commitType, string expected)
        => Assert.Equal(expected, WorktreeManager.BranchTypeFor(commitType));

In CommitMessageBuilderTests.cs einen Test ergänzen:

    [Fact]
    public void ToSlug_transliterates_umlauts()
    {
        Assert.Equal("aerger-ueber-groesse-strasse", CommitMessageBuilder.ToSlug("Ärger über Größe Straße"));
    }
  • Step 2: Build — muss scheitern

Run: dotnet build ClaudeDo.slnx -c Release Expected: FAIL, NameFor hat keine 3-Parameter-Überladung, BranchTypeFor fehlt.

  • Step 3: Implementieren

CommitMessageBuilder.ToSlug — erste Zeile ersetzen:

        // Umlaute auflösen statt strippen: "Löschen" -> "loeschen", nicht "lschen".
        var lower = name.ToLowerInvariant()
            .Replace("ä", "ae").Replace("ö", "oe").Replace("ü", "ue").Replace("ß", "ss");

WorktreeManager.cs — NameFor (Zeilen 41-47) ersetzen durch:

    /// <summary>
    /// Branch leaf and folder name: <c>{nummer}-{slug}</c>. Nummer = Ticket-Id, sonst die Task-Nummer —
    /// außer im Firmen-Repo ohne Ticket, dort sähe sie wie eine Ticket-Id aus und entfällt.
    /// </summary>
    public static string NameFor(TaskEntity task, int? ticketId, bool isCompanyRepo)
    {
        var titleSlug = CommitMessageBuilder.ToSlug(task.Title);
        if (titleSlug.Length > 40) titleSlug = titleSlug[..40].TrimEnd('-');
        var number = ticketId?.ToString() ?? (isCompanyRepo ? null : task.Number.ToString());
        if (titleSlug.Length == 0) return number ?? task.Number.ToString();
        return number is null ? titleSlug : $"{number}-{titleSlug}";
    }

    /// <summary>Branch-Präfix nach work-conventions: <c>feat</c> → <c>feature</c>, Typen ohne eigenes Präfix → <c>chore</c>.</summary>
    public static string BranchTypeFor(string? commitType)
    {
        var t = (commitType ?? "").Trim().ToLowerInvariant();
        return t switch
        {
            "feat" => "feature",
            "fix" or "refactor" or "docs" or "test" or "chore" => t,
            _ => "chore",
        };
    }

In CreateAsync vorerst nur den Aufruf kompilierbar machen (Zeile 62): var name = NameFor(task, null, false); — die echte Verdrahtung macht Task 3.

Die übrigen NameFor(task)-Aufrufe in WorktreeManagerTests.cs (Zeilen 131, 137, 160, 314, 320) auf WorktreeManager.NameFor(task, null, false) umstellen — sonst kompiliert das Testprojekt nicht. Die claudedo/-Literale dort bleiben in diesem Task stehen (Task 3 ändert sie).

  • Step 4: Build + Test

Run: dotnet build ClaudeDo.slnx -c Release → 0 Fehler. Run: dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~WorktreeManagerTests|FullyQualifiedName~CommitMessageBuilderTests" Expected: PASS.

  • Step 5: Commit
git add src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs src/ClaudeDo.Worker/Runner/WorktreeManager.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs tests/ClaudeDo.Worker.Tests/Runner/CommitMessageBuilderTests.cs
git commit -m "feat(worker): branch type and number rules for task branches" -- src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs src/ClaudeDo.Worker/Runner/WorktreeManager.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs tests/ClaudeDo.Worker.Tests/Runner/CommitMessageBuilderTests.cs

Task 3: CreateAsync nutzt das neue Schema

Files:

  • Modify: src/ClaudeDo.Worker/Runner/WorktreeManager.cs:49-90 (CreateAsync) + neue private ResolveTicketIdAsync

  • Test: tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs

  • Step 1: Tests anpassen + neue Tests

Bestehende Literale umstellen (der Test-Task hat CommitType = "chore", also Präfix chore/):

  • Zeile 69: Assert.StartsWith($"chore/{task.Number}-test-task", ctx.BranchName);
  • Zeile 131: var foreign = $"chore/{WorktreeManager.NameFor(task, null, false)}";
  • Zeile 138: Assert.Equal($"chore/{suffixed}", ctx.BranchName);
  • Zeile 160: var branchName = $"chore/{WorktreeManager.NameFor(task, null, false)}";
  • Zeile 314: var branchName = $"chore/{WorktreeManager.NameFor(task, null, false)}";

Neue Tests in derselben Klasse (vor MakeEntities):

    [Fact]
    public async Task CreateAsync_CompanyRepoWithTicket_UsesTicketIdAndFeaturePrefix()
    {
        if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }

        var repo = CreateRepo();
        GitRepoFixture.RunGit(repo.RepoDir, "remote", "add", "origin", "https://srv.local/Coll/Proj/_git/Repo");
        var (task, list) = MakeEntities(repo.RepoDir);
        task.CommitType = "feat";
        task.TicketRef = "bandel:412";
        var (mgr, _) = await CreateManagerAsync(task, list);

        var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
        _worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));

        Assert.StartsWith("feature/412-test-task", ctx.BranchName);
    }

    [Fact]
    public async Task CreateAsync_CompanyRepoWithoutTicket_OmitsNumber()
    {
        if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }

        var repo = CreateRepo();
        GitRepoFixture.RunGit(repo.RepoDir, "remote", "add", "origin", "https://srv.local/Coll/Proj/_git/Repo");
        var (task, list) = MakeEntities(repo.RepoDir);
        var (mgr, _) = await CreateManagerAsync(task, list);

        var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
        _worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));

        Assert.StartsWith("chore/test-task", ctx.BranchName);
        Assert.DoesNotContain($"/{task.Number}-", ctx.BranchName);
    }

    [Fact]
    public async Task CreateAsync_ChildWithoutTicket_UsesParentTicket()
    {
        if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }

        var repo = CreateRepo();
        var (child, list) = MakeEntities(repo.RepoDir);
        var parent = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(), ListId = list.Id, Title = "parent", CommitType = "feat",
            TicketRef = "bandel:77", CreatedAt = DateTime.UtcNow,
        };
        child.ParentTaskId = parent.Id;

        var db = new DbFixture();
        _dbFixtures.Add(db);
        using (var seedCtx = db.CreateContext())
        {
            await new ListRepository(seedCtx).AddAsync(list);
            await new TaskRepository(seedCtx).AddAsync(parent);
            await new TaskRepository(seedCtx).AddAsync(child);
        }
        var mgr = new WorktreeManager(new GitService(), db.CreateFactory(),
            new WorkerConfig { WorktreeRootStrategy = "sibling" }, NullLogger<WorktreeManager>.Instance);

        var ctx = await mgr.CreateAsync(child, list, CancellationToken.None);
        _worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));

        Assert.StartsWith("chore/77-test-task", ctx.BranchName);
    }
  • Step 2: Test laufen lassen — muss scheitern

Run: dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~WorktreeManagerTests" Expected: FAIL — Branches heißen noch claudedo/….

  • Step 3: Implementieren

In CreateAsync Zeile 62 (var name = NameFor(task, null, false);) ersetzen durch:

        var isCompany = await CompanyRepo.IsCompanyAsync(_git, workingDir, _cfg.CompanyRemotePattern, ct);
        var name = NameFor(task, await ResolveTicketIdAsync(task, ct), isCompany);
        var branchType = BranchTypeFor(task.CommitType);

Kollisionscheck + Branchname (Zeilen 80-89) ersetzen durch:

        // Numbers restart after a DB reset, and without the claudedo/ prefix a taken name may also be
        // the user's own branch — the self-heal below would force-delete it, the id suffix keeps it untouched.
        if (Directory.Exists(Path.Combine(parentDir, name))
            || (await _git.ListLocalBranchesAsync(workingDir, ct)).Contains($"{branchType}/{name}"))
        {
            var id = task.Id.Replace("-", "");
            name = $"{name}-{id[..Math.Min(8, id.Length)]}";
        }

        var branchName = $"{branchType}/{name}";

Neue private Methode (vor ResolveBaseCommitAsync):

    /// <summary>Ticket-Id des Tasks, sonst die seines Parents (Children erben das Ticket ihrer Unit).</summary>
    private async Task<int?> ResolveTicketIdAsync(TaskEntity task, CancellationToken ct)
    {
        var own = TicketStatusMap.ParseBandelTicketId(task.TicketRef);
        if (own is not null || task.ParentTaskId is null) return own;
        using var ctx = _dbFactory.CreateDbContext();
        var parentRef = await ctx.Tasks.AsNoTracking()
            .Where(t => t.Id == task.ParentTaskId)
            .Select(t => t.TicketRef)
            .FirstOrDefaultAsync(ct);
        return TicketStatusMap.ParseBandelTicketId(parentRef);
    }

Usings ergänzen: using ClaudeDo.Worker.Tickets; (für TicketStatusMap). ClaudeDo.Worker.Git ist schon importiert.

  • Step 4: Build + Test

Run: dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~WorktreeManagerTests|FullyQualifiedName~InteractiveLaunchSpecServiceTests|FullyQualifiedName~TaskRunnerTests" Expected: PASS. Schlägt ein Test fehl, der einen claudedo/{nummer}-…-Namen erwartet (nicht nur als Fixture seedet), auf chore/… umstellen und im Bericht nennen.

  • Step 5: Commit
git add src/ClaudeDo.Worker/Runner/WorktreeManager.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs
git commit -m "feat(worker): name task branches <type>/<number>-<slug>" -- src/ClaudeDo.Worker/Runner/WorktreeManager.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs

Task 4: UntrackedWorktreeRecovery ohne Präfix

Files:

  • Modify: src/ClaudeDo.Worker/Lifecycle/UntrackedWorktreeRecovery.cs

  • Test: tests/ClaudeDo.Worker.Tests/Lifecycle/UntrackedWorktreeRecoveryTests.cs (bestehende Datei; per Glob tests/**/UntrackedWorktreeRecoveryTests.cs finden und dort ergänzen)

  • Step 1: Failing test

    [Theory]
    [InlineData(@"C:\src\.claudedo-worktrees\lager-app\412-fix", @"C:\Users\x\.claudeDo\worktrees", true)]
    [InlineData(@"C:\src\.claudedo-worktrees\planning\abc123", @"C:\Users\x\.claudeDo\worktrees", false)]
    [InlineData(@"C:\Users\x\.claudeDo\worktrees\lager-app\412-fix", @"C:\Users\x\.claudeDo\worktrees", true)]
    [InlineData(@"C:\Users\x\.claudeDo\worktrees\planning\abc123", @"C:\Users\x\.claudeDo\worktrees", false)]
    [InlineData(@"C:\src\my-own-worktree", @"C:\Users\x\.claudeDo\worktrees", false)]
    public void IsTaskWorktreePath(string path, string centralRoot, bool expected)
    {
        if (!OperatingSystem.IsWindows()) return;
        Assert.Equal(expected, UntrackedWorktreeRecovery.IsTaskWorktreePath(path, centralRoot));
    }
  • Step 2: Build — scheitert (IsTaskWorktreePath fehlt).

Run: dotnet build ClaudeDo.slnx -c Release → FAIL.

  • Step 3: Implementieren

Konstante BranchPrefix löschen. Konstruktor um WorkerConfig cfg erweitern (Feld _cfg; using ClaudeDo.Worker.Config;). Klassen-Summary auf „ClaudeDo-Task-Worktrees (unter .claudedo-worktrees oder dem Central-Root, ohne Planning-Sessions)“ ändern.

In StartAsync nach dem Laden der Listen den Central-Root bestimmen:

        var settings = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
        var centralRoot = !string.IsNullOrWhiteSpace(settings.CentralWorktreeRoot)
            ? settings.CentralWorktreeRoot
            : _cfg.CentralWorktreeRoot;

Den Filter claudedoPaths ersetzen:

            var claudedoPaths = registered
                .Where(w => w.Branch is not null && IsTaskWorktreePath(w.Path, centralRoot))
                .Select(w => w.Path);

Neue statische Methode (neben FindUntrackedPaths):

    /// <summary>True für einen Pfad unter <c>.claudedo-worktrees</c> oder dem Central-Root, außer Planning-Session-Worktrees.</summary>
    public static bool IsTaskWorktreePath(string path, string centralRoot)
    {
        var sep = Path.DirectorySeparatorChar;
        var full = Normalize(path);
        string? rest = null;

        var central = Normalize(centralRoot);
        if (full.StartsWith(central + sep, StringComparison.OrdinalIgnoreCase))
            rest = full[(central.Length + 1)..];
        else
        {
            var marker = $"{sep}.claudedo-worktrees{sep}";
            var idx = full.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
            if (idx >= 0) rest = full[(idx + marker.Length)..];
        }

        return rest is not null && !rest.StartsWith($"planning{sep}", StringComparison.OrdinalIgnoreCase);
    }
  • Step 4: Build + Test

Run: dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~UntrackedWorktreeRecovery" Expected: PASS.

  • Step 5: Commit
git add src/ClaudeDo.Worker/Lifecycle/UntrackedWorktreeRecovery.cs tests/ClaudeDo.Worker.Tests/Lifecycle/UntrackedWorktreeRecoveryTests.cs
git commit -m "refactor(worker): detect untracked worktrees by path instead of branch prefix" -- src/ClaudeDo.Worker/Lifecycle/UntrackedWorktreeRecovery.cs tests/ClaudeDo.Worker.Tests/Lifecycle/UntrackedWorktreeRecoveryTests.cs

(Pfad der Testdatei an den tatsächlichen Fundort anpassen.)


Task 5: list_config.base_branch durch alle Writer

Files:

  • Modify: src/ClaudeDo.Data/Models/ListConfigEntity.cs, src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs, src/ClaudeDo.Data/Repositories/ListRepository.cs:161-180

  • Create: Migration AddListBaseBranch (generiert)

  • Modify: src/ClaudeDo.Data/Wire.cs:111,115

  • Modify: src/ClaudeDo.Worker/Hub/WorkerHub.cs:577-647

  • Modify: src/ClaudeDo.Worker/External/ConfigMcpTools.cs:13,53-59,104-139

  • Test: tests/ClaudeDo.Worker.Tests/Hub/ListConfigBaseBranchTests.cs

  • Step 1: Failing tests

tests/ClaudeDo.Worker.Tests/Hub/ListConfigBaseBranchTests.cs — Hub-Aufbau 1:1 aus tests/ClaudeDo.Worker.Tests/Tickets/ListConfigTicketProjectTests.cs (gleicher CreateHub, SeedListAsync, ReadConfigAsync):

using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.External;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Tests.Infrastructure;
using Xunit;

namespace ClaudeDo.Worker.Tests.Hub;

/// BaseBranch und PowerUseAllowed sitzen in der SetConfigAsync-kopiert-verbatim-Falle: jeder Writer,
/// der sie nicht durchreicht, setzt sie still zurück.
public sealed class ListConfigBaseBranchTests : IDisposable
{
    private readonly DbFixture _db = new();
    public void Dispose() => _db.Dispose();

    private WorkerHub CreateHub()
    {
        var factory = _db.CreateFactory();
        var broadcaster = new HubBroadcaster(new CapturingHubContext());
        var hub = new WorkerHub(
            null!, null!, null!, null!, broadcaster, factory,
            null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
            null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
            new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
        hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
        hub.Context = new FakeHubCallerContext();
        return hub;
    }

    private async Task<string> SeedListAsync(string? baseBranch, bool powerUse = false)
    {
        var listId = Guid.NewGuid().ToString();
        await using var ctx = _db.CreateContext();
        var repo = new ListRepository(ctx);
        await repo.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
        await repo.SetConfigAsync(new ListConfigEntity { ListId = listId, BaseBranch = baseBranch, PowerUseAllowed = powerUse });
        return listId;
    }

    private async Task<ListConfigEntity?> ReadConfigAsync(string listId)
    {
        await using var ctx = _db.CreateContext();
        return await new ListRepository(ctx).GetConfigAsync(listId);
    }

    [Fact]
    public async Task set_list_config_preserves_BaseBranch_and_PowerUseAllowed()
    {
        var listId = await SeedListAsync("dev", powerUse: true);
        var factory = _db.CreateFactory();
        await using var ctx = factory.CreateDbContext();
        var tool = new ConfigMcpTools(new ListRepository(ctx), new TaskRepository(ctx),
            new HubBroadcaster(new CapturingHubContext()), factory);

        await tool.SetListConfig(listId, model: "opus", cancellationToken: CancellationToken.None);

        var cfg = await ReadConfigAsync(listId);
        Assert.Equal("dev", cfg!.BaseBranch);
        Assert.True(cfg.PowerUseAllowed);
    }

    [Fact]
    public async Task get_list_config_returns_BaseBranch()
    {
        var listId = await SeedListAsync("dev");
        var factory = _db.CreateFactory();
        await using var ctx = factory.CreateDbContext();
        var tool = new ConfigMcpTools(new ListRepository(ctx), new TaskRepository(ctx),
            new HubBroadcaster(new CapturingHubContext()), factory);

        var result = await tool.GetListConfig(listId, CancellationToken.None);

        Assert.Equal("dev", result.Config!.BaseBranch);
    }

    [Fact]
    public async Task UpdateListConfig_null_keeps_BaseBranch()
    {
        var listId = await SeedListAsync("dev");
        await CreateHub().UpdateListConfig(new UpdateListConfigDto(listId, "opus", null, null));
        Assert.Equal("dev", (await ReadConfigAsync(listId))!.BaseBranch);
    }

    [Fact]
    public async Task UpdateListConfig_empty_clears_BaseBranch()
    {
        var listId = await SeedListAsync("dev");
        await CreateHub().UpdateListConfig(new UpdateListConfigDto(listId, "opus", null, null, BaseBranch: ""));
        Assert.Null((await ReadConfigAsync(listId))!.BaseBranch);
    }

    [Fact]
    public async Task UpdateListConfig_sets_BaseBranch_and_GetListConfig_returns_it()
    {
        var listId = await SeedListAsync(null);
        var hub = CreateHub();
        await hub.UpdateListConfig(new UpdateListConfigDto(listId, null, null, null, BaseBranch: "dev"));
        Assert.Equal("dev", (await hub.GetListConfig(listId))!.BaseBranch);
    }
}
  • Step 2: Build — scheitert (BaseBranch fehlt).

  • Step 3: Implementieren

ListConfigEntity.cs, nach PowerUseAllowed:

    /// <summary>
    /// Basis neuer Worktrees und Default-Merge-/PR-Ziel (z.B. <c>dev</c>). Null = ausgecheckter Branch
    /// (privat) bzw. Default-Branch des Remotes (Firma). ⚠️ SetConfigAsync kopiert verbatim.
    /// </summary>
    public string? BaseBranch { get; set; }

ListConfigEntityConfiguration.cs: builder.Property(c => c.BaseBranch).HasColumnName("base_branch");

ListRepository.SetConfigAsync, im else-Zweig: existing.BaseBranch = config.BaseBranch;

Migration erzeugen:

dotnet ef migrations add AddListBaseBranch --project src/ClaudeDo.Data/ClaudeDo.Data.csproj --configuration Release

Erwartet: neue Migration mit genau einem AddColumn<string>(name: "base_branch", table: "list_config", nullable: true). Steht mehr drin (fremde Snapshot-Drift), abbrechen und melden.

Wire.cs — Kommentar über UpdateListConfigDto um eine Zeile ergänzen: // BaseBranch: null = keep stored, "" = clear, value = set. Dann beide Records erweitern:

public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null, bool? PowerUseAllowed = null, string? BaseBranch = null);
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null, bool PowerUseAllowed = false, string? BaseBranch = null);

WorkerHub.UpdateListConfig — nach der powerUseAllowed-Zeile:

        // Tri-State wie oben: null = behalten, "" = löschen.
        var baseBranch = dto.BaseBranch is null ? existing?.BaseBranch : dto.BaseBranch.NullIfBlank();

Leer-Prüfung um && baseBranch is null ergänzen, im new ListConfigEntity { … } BaseBranch = baseBranch, ergänzen. GetListConfig: config.BaseBranch als letztes Argument an new ListConfigDto(…) hängen.

ConfigMcpTools.cs:

  • Zeile 13: public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns, string? VerifyCommand, string? BaseBranch = null);
  • GetListConfig: new ListConfigDto(cfg.Model, cfg.SystemPrompt, cfg.AgentPath, cfg.MaxTurns, cfg.VerifyCommand, cfg.BaseBranch)
  • hasUnrelatedSettings erweitern:
        var hasUnrelatedSettings = existing is not null
            && (existing.SessionSkills is not null
                || existing.PermissionMode is not null || existing.TicketProjectId is not null
                || existing.BaseBranch is not null || existing.PowerUseAllowed);
  • im SetConfigAsync(new ListConfigEntity { … }) ergänzen: BaseBranch = existing?.BaseBranch, PowerUseAllowed = existing?.PowerUseAllowed ?? false,

  • config = allCleared ? null : new ListConfigDto(m, sp, ap, mt, vc, existing?.BaseBranch);

  • Step 4: Build + Test

Run: dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ListConfigBaseBranchTests|FullyQualifiedName~ListConfigTicketProjectTests|FullyQualifiedName~ListConfigHubTests|FullyQualifiedName~ExternalMcpToolSchemaTests" Run: dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~Migration" Expected: PASS.

  • Step 5: Commit
git add src/ClaudeDo.Data/Models/ListConfigEntity.cs src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs src/ClaudeDo.Data/Repositories/ListRepository.cs src/ClaudeDo.Data/Migrations src/ClaudeDo.Data/Wire.cs src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Worker/External/ConfigMcpTools.cs tests/ClaudeDo.Worker.Tests/Hub/ListConfigBaseBranchTests.cs
git commit -m "feat(data): per-list base branch, preserved by every config writer" -- src/ClaudeDo.Data/Models/ListConfigEntity.cs src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs src/ClaudeDo.Data/Repositories/ListRepository.cs src/ClaudeDo.Data/Migrations src/ClaudeDo.Data/Wire.cs src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Worker/External/ConfigMcpTools.cs tests/ClaudeDo.Worker.Tests/Hub/ListConfigBaseBranchTests.cs

Task 6: Basis-Branch als Worktree-Basis und Merge-Ziel

Files:

  • Modify: src/ClaudeDo.Worker/Runner/WorktreeManager.cs (ResolveBaseCommitAsync, Fallback-Zeile)

  • Modify: src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs (GetTargetsAsync ~869, PreviewAsync ~910, ApproveAndMergeAsync ~1077, MergeAsync ~375)

  • Test: tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs, tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs

  • Step 1: Failing tests

WorktreeManagerTests.cs:

    [Fact]
    public async Task CreateAsync_BaseBranchSet_BasesOnItEvenWhenAnotherBranchIsCheckedOut()
    {
        if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }

        var repo = CreateRepo();
        GitRepoFixture.RunGit(repo.RepoDir, "checkout", "-b", "dev");
        File.WriteAllText(Path.Combine(repo.RepoDir, "dev.txt"), "dev");
        GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
        GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "dev work");
        var devHead = GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim();
        GitRepoFixture.RunGit(repo.RepoDir, "checkout", "-b", "elsewhere", repo.BaseCommit);

        var (task, list) = MakeEntities(repo.RepoDir);
        var (mgr, db) = await CreateManagerAsync(task, list);
        using (var ctx0 = db.CreateContext())
            await new ListRepository(ctx0).SetConfigAsync(new ListConfigEntity { ListId = list.Id, BaseBranch = "dev" });

        var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
        _worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));

        Assert.Equal(devHead, ctx.BaseCommit);
    }

TaskMergeServiceTests.cs — am vorhandenen Setup der Klasse orientieren (Repo + SeedWorktree-Helper, wie die übrigen Tests der Datei). Ein Test, der zeigt, dass GetTargetsAsync BaseBranch als Default liefert, auch wenn ein anderer Branch ausgecheckt ist:

    [Fact]
    public async Task GetTargetsAsync_prefers_list_base_branch()
    {
        // Setup wie die anderen Tests dieser Datei: Repo, Liste, Task mit Worktree (SeedWorktree).
        // Zusätzlich: Branch "dev" anlegen (`git branch dev`), list_config.BaseBranch = "dev" setzen,
        // Main-Checkout bleibt auf seinem Default-Branch.
        // Assert: targets.DefaultBranch == "dev".
    }

Der Implementer schreibt diesen Test konkret mit dem Fixture-Setup, das in TaskMergeServiceTests.cs schon für GetTargetsAsync/Merge-Tests verwendet wird (per Grep "GetTargetsAsync" tests/ClaudeDo.Worker.Tests den nächstliegenden bestehenden Test kopieren und um BaseBranch ergänzen). Ein zweiter Test: ApproveAndMergeAsync(taskId, "") mit BaseBranch = "dev" merged auf dev (git log dev --oneline enthält den Merge-Commit, der vorher ausgecheckte Branch nicht).

  • Step 2: Tests laufen lassen — scheitern

Run: dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~BaseBranch|FullyQualifiedName~prefers_list_base_branch" Expected: FAIL.

  • Step 3: Implementieren

WorktreeManager.ResolveBaseCommitAsync — die letzte Zeile return await _git.RevParseHeadAsync(workingDir, ct); ersetzen:

        using var cfgCtx = _dbFactory.CreateDbContext();
        var baseBranch = await cfgCtx.ListConfigs.AsNoTracking()
            .Where(c => c.ListId == task.ListId)
            .Select(c => c.BaseBranch)
            .FirstOrDefaultAsync(ct);
        return string.IsNullOrWhiteSpace(baseBranch)
            ? await _git.RevParseHeadAsync(workingDir, ct)
            : await _git.RevParseAsync(workingDir, baseBranch, ct);

(Ein nicht existierender Branch wirft aus RevParseAsync mit „git rev-parse 'dev' failed“ — gewollt, der Lauf scheitert mit lesbarer Meldung.)

TaskMergeService — neue öffentliche Methode (bei GetTargetsAsync):

    /// <summary>Leeres Ziel → <c>list_config.base_branch</c>, sonst der ausgecheckte Branch des Main-Checkouts.</summary>
    public async Task<string> ResolveTargetAsync(ListEntity list, string? targetBranch, CancellationToken ct)
    {
        if (!string.IsNullOrWhiteSpace(targetBranch)) return targetBranch;
        using var ctx = _dbFactory.CreateDbContext();
        var baseBranch = await ctx.ListConfigs.AsNoTracking()
            .Where(c => c.ListId == list.Id)
            .Select(c => c.BaseBranch)
            .FirstOrDefaultAsync(ct);
        return string.IsNullOrWhiteSpace(baseBranch)
            ? await _git.GetCurrentBranchAsync(list.WorkingDir!, ct)
            : baseBranch;
    }

Aufrufer umstellen:

  • GetTargetsAsync: var current = await _git.GetCurrentBranchAsync(...) → var current = await ResolveTargetAsync(list, null, ct);

  • PreviewAsync (~910) und ApproveAndMergeAsync (~1077): die var target = string.IsNullOrWhiteSpace(targetBranch) ? await _git.GetCurrentBranchAsync(...) : targetBranch;-Blöcke → var target = await ResolveTargetAsync(list, targetBranch, ct);

  • MergeAsync: direkt nachdem list.WorkingDir geprüft ist und vor dem currentBranch-Vergleich (~Zeile 414) targetBranch = await ResolveTargetAsync(list, targetBranch, ct); einfügen. Grund: PlanningMergeOrchestrator.DrainAsync reicht ein leeres Ziel (MCP review_task ohne targetBranch) unverändert durch, was heute in git checkout "" → Blocked endet. Mit der zentralen Auflösung ist das für alle Aufrufer behoben.

  • Step 4: Build + Test

Run: dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~WorktreeManagerTests|FullyQualifiedName~TaskMergeServiceTests|FullyQualifiedName~PlanningMergeOrchestratorTests" Expected: PASS.

  • Step 5: Commit
git add src/ClaudeDo.Worker/Runner/WorktreeManager.cs src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs
git commit -m "feat(worker): use the list base branch as worktree base and default merge target" -- src/ClaudeDo.Worker/Runner/WorktreeManager.cs src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs

Task 7: UI — Basis-Branch-Feld + echte Branchnamen

Files:

  • Modify: src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs:216-231

  • Modify: src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs (Property ~57, Load ~276, Save ~375)

  • Modify: src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml (nach dem WorkingDir-StackPanel, ~Zeile 125)

  • Modify: src/ClaudeDo.Localization/locales/en.json, de.json (Block modals.listSettings, nach workingDirectoryPlaceholder)

  • Modify: src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs:670,933,1223,1385

  • Test: tests/ClaudeDo.Ui.Tests — bestehende ListSettingsModal-Tests per Grep "ListSettingsModalViewModel" tests/ClaudeDo.Ui.Tests finden

  • Step 1: Failing test

In der vorhandenen ListSettingsModal-Testklasse (gleiche Fake-IWorkerClient-Mechanik wie die dortigen Save-Tests, die UpdateListConfigDto mitschneiden):

    [Fact]
    public async Task Save_sends_BaseBranch_explicitly()
    {
        // Arrange: VM wie in den Nachbartests laden; BaseBranch = "dev" setzen.
        // Act: Save ausführen.
        // Assert: der mitgeschnittene UpdateListConfigDto hat BaseBranch == "dev".
        // Zweiter Fall: BaseBranch = "" → DTO.BaseBranch == "" (nicht null), damit Leeren wirklich löscht.
    }

Der Implementer schreibt die beiden Fälle konkret gegen den Fake, den die Nachbartests (z.B. der PowerUseAllowed-Save-Test) verwenden.

  • Step 2: Build/Test — scheitert (BaseBranch fehlt im VM).

  • Step 3: Implementieren

AgentConfigEditorViewModel.SaveAsync: Signatur um string? baseBranch = null erweitern und im List-Zweig durchreichen:

    public async System.Threading.Tasks.Task SaveAsync(
        string? verifyCommand = null, int? ticketProjectId = null, bool? powerUseAllowed = null, string? baseBranch = null)
            await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand, permission, ticketProjectId, powerUseAllowed, baseBranch));

ListSettingsModalViewModel:

    // Basis neuer Worktrees und Default-Merge-Ziel; leer = ausgecheckter Branch.
    [ObservableProperty] private string _baseBranch = "";

Load (nach VerifyCommand = …): BaseBranch = cfg?.BaseBranch ?? ""; Save: await Agent.SaveAsync(…, PowerUseAllowed, BaseBranch.Trim()); — immer explizit, "" löscht (das Modal besitzt das Feld).

View, nach dem WorkingDir-StackPanel:

              <StackPanel Spacing="4" IsVisible="{Binding !IsFolder}">
                <TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.baseBranch}"/>
                <TextBox Text="{Binding BaseBranch, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                         PlaceholderText="{loc:Tr modals.listSettings.baseBranchPlaceholder}"/>
                <TextBlock Text="{loc:Tr modals.listSettings.baseBranchHint}"
                           Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
              </StackPanel>

Locales (beide Dateien, gleiche Keys, direkt nach workingDirectoryPlaceholder):

en.json:

      "baseBranch": "Base branch",
      "baseBranchPlaceholder": "(checked-out branch)",
      "baseBranchHint": "New task branches start here and approved work merges back here, e.g. dev. Leave empty to use whatever branch is checked out.",

de.json:

      "baseBranch": "Basis-Branch",
      "baseBranchPlaceholder": "(ausgecheckter Branch)",
      "baseBranchHint": "Neue Task-Branches starten hier, und freigegebene Arbeit wird hierhin gemergt, z.B. dev. Leer = der gerade ausgecheckte Branch.",

Nach dem Editieren beide JSON-Dateien separat parsen (python -c "import json;json.load(open('src/ClaudeDo.Localization/locales/de.json',encoding='utf-8'))"), weil ein kaputtes de.json vom LocaleStore still geschluckt wird.

DetailsIslandViewModel:

  • Zeilen 670 und 933: BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null; → BranchLine = entity.Worktree?.BranchName; (das Ziel ist nicht mehr immer main).

  • Zeilen 1223 und 1385: var branchName = $"claudedo/{Task.Id.Replace("-", "")}"; → var branchName = BranchLine ?? "the task branch";

  • Step 4: Build + Test

Run: dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ListSettings|FullyQualifiedName~DetailsIsland" Run: dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release --no-build Expected: PASS.

  • Step 5: Commit
git add src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml src/ClaudeDo.Localization/locales/en.json src/ClaudeDo.Localization/locales/de.json src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs <ui-testdatei>
git commit -m "feat(ui): base branch field in list settings, real branch names in dialogs" -- <dieselben pfade>

Sichtprüfung offen (Mika): Feld im Listen-Modal, Branchzeile im Detail-Pane.


Task 8: Doku

Files:

  • Modify: src/ClaudeDo.Worker/CLAUDE.md (Key Components → WorktreeManager; Notes → „Task worktree branches follow…“; External → „Task worktree branches and folders are named…“; Config → company_remote_pattern)

  • Modify: src/ClaudeDo.Data/CLAUDE.md (list_config: base_branch)

  • Step 1: Texte ersetzen

Überall claudedo/{number}-{title-slug} → <typ>/<nummer>-<slug> mit der Regel: Typ aus WorktreeManager.BranchTypeFor (feat→feature, sonst fix/refactor/docs/test/chore, Rest chore); Nummer = Ticket-Id (eigene oder des Parents), sonst Task-Nummer, im Firmen-Repo ohne Ticket keine. Planning bleibt claudedo/planning/{guid}.

Config-Abschnitt ergänzen: `company_remote_pattern` (`/_git/`) — Teilstring der origin-URL, an dem `CompanyRepo` ein Firmen-Repo erkennt; nur per Hand editierbar.

Data/CLAUDE.md: list_config.base_branch — Basis neuer Worktrees + Default-Merge-Ziel, tri-state in UpdateListConfigDto, ⚠️ verbatim-copy-Falle wie ticket_project_id.

  • Step 2: Commit
git add src/ClaudeDo.Worker/CLAUDE.md src/ClaudeDo.Data/CLAUDE.md
git commit -m "docs(worker): document new branch naming and per-list base branch" -- src/ClaudeDo.Worker/CLAUDE.md src/ClaudeDo.Data/CLAUDE.md