# 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 `/-` 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 -c Release --no-build --filter ""` — nie die volle Suite. - Commit: `git add ` explizit, dann `git commit -m "..." -- ` (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** ```csharp 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: ```csharp /// Teilstring der origin-URL, an dem ein Firmen-Repo erkannt wird. Nur per Hand editierbar. [JsonPropertyName("company_remote_pattern")] public string CompanyRemotePattern { get; set; } = "/_git/"; ``` `GitService.cs`, neben `ListLocalBranchesAsync`: ```csharp /// URL des Remotes , oder null wenn es keins gibt. public async Task 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`: ```csharp using ClaudeDo.Data.Git; namespace ClaudeDo.Worker.Git; /// Erkennt Firmen-Repos am origin-Remote; das Muster kommt aus WorkerConfig.CompanyRemotePattern. public static class CompanyRepo { /// True wenn das (nicht-leere) Muster enthält, ohne Groß-/Kleinschreibung. public static bool Matches(string? remoteUrl, string? pattern) => !string.IsNullOrWhiteSpace(remoteUrl) && !string.IsNullOrWhiteSpace(pattern) && remoteUrl.Contains(pattern, StringComparison.OrdinalIgnoreCase); /// Liest origin live (kein Cache — ein Remote kann sich jederzeit ändern). public static async Task 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** ```bash 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: ```csharp [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: ```csharp [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: ```csharp // 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: ```csharp /// /// Branch leaf and folder name: {nummer}-{slug}. 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. /// 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}"; } /// Branch-Präfix nach work-conventions: feat → feature, Typen ohne eigenes Präfix → chore. 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** ```bash 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`): ```csharp [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.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: ```csharp 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: ```csharp // 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`): ```csharp /// Ticket-Id des Tasks, sonst die seines Parents (Children erben das Ticket ihrer Unit). private async Task 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** ```bash git add src/ClaudeDo.Worker/Runner/WorktreeManager.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs git commit -m "feat(worker): name task branches /-" -- 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** ```csharp [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: ```csharp var settings = await new AppSettingsRepository(ctx).GetAsync(cancellationToken); var centralRoot = !string.IsNullOrWhiteSpace(settings.CentralWorktreeRoot) ? settings.CentralWorktreeRoot : _cfg.CentralWorktreeRoot; ``` Den Filter `claudedoPaths` ersetzen: ```csharp var claudedoPaths = registered .Where(w => w.Branch is not null && IsTaskWorktreePath(w.Path, centralRoot)) .Select(w => w.Path); ``` Neue statische Methode (neben `FindUntrackedPaths`): ```csharp /// True für einen Pfad unter .claudedo-worktrees oder dem Central-Root, außer Planning-Session-Worktrees. 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** ```bash 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`): ```csharp 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 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 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`: ```csharp /// /// Basis neuer Worktrees und Default-Merge-/PR-Ziel (z.B. dev). Null = ausgecheckter Branch /// (privat) bzw. Default-Branch des Remotes (Firma). ⚠️ SetConfigAsync kopiert verbatim. /// 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: ```bash dotnet ef migrations add AddListBaseBranch --project src/ClaudeDo.Data/ClaudeDo.Data.csproj --configuration Release ``` Erwartet: neue Migration mit genau einem `AddColumn(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: ```csharp public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null, bool? PowerUseAllowed = null, string? BaseBranch = null); ``` ```csharp public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null, bool PowerUseAllowed = false, string? BaseBranch = null); ``` `WorkerHub.UpdateListConfig` — nach der `powerUseAllowed`-Zeile: ```csharp // 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: ```csharp 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** ```bash 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`: ```csharp [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: ```csharp [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: ```csharp 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`): ```csharp /// Leeres Ziel → list_config.base_branch, sonst der ausgecheckte Branch des Main-Checkouts. public async Task 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** ```bash 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): ```csharp [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: ```csharp public async System.Threading.Tasks.Task SaveAsync( string? verifyCommand = null, int? ticketProjectId = null, bool? powerUseAllowed = null, string? baseBranch = null) ``` ```csharp await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand, permission, ticketProjectId, powerUseAllowed, baseBranch)); ``` `ListSettingsModalViewModel`: ```csharp // 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: ```xml ``` Locales (beide Dateien, gleiche Keys, direkt nach `workingDirectoryPlaceholder`): en.json: ```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: ```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** ```bash 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 git commit -m "feat(ui): base branch field in list settings, real branch names in dialogs" -- ``` **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}` → `/-` 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** ```bash 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 ```