From 48899b3df86f0c44e716d89126ade71d1345a2c5 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Fri, 24 Apr 2026 11:43:53 +0200 Subject: [PATCH] feat(worker): cleanup planning worktree and branch on finalize/discard Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Planning/PlanningSessionManager.cs | 69 +++++++++++++++++-- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs b/src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs index 0286a0f..6dc4a2a 100644 --- a/src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs +++ b/src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs @@ -140,9 +140,21 @@ public sealed class PlanningSessionManager public async Task FinalizeAsync(string taskId, bool queueAgentTasks, CancellationToken ct) { - var (tasks, _, settings, ctx) = CreateRepos(); + var (tasks, lists, settings, ctx) = CreateRepos(); await using var __ = ctx; - return await tasks.FinalizePlanningAsync(taskId, queueAgentTasks, ct); + + var count = await tasks.FinalizePlanningAsync(taskId, queueAgentTasks, ct); + + // Best-effort cleanup — don't block finalization on git state. + await TryCleanupWorktreeAsync(taskId, lists, settings, ct); + + var sessionDir = Path.Combine(_rootDirectory, taskId); + if (Directory.Exists(sessionDir)) + { + try { Directory.Delete(sessionDir, recursive: true); } catch { } + } + + return count; } public async Task GetPendingDraftCountAsync(string taskId, CancellationToken ct) @@ -155,16 +167,19 @@ public sealed class PlanningSessionManager public async Task DiscardAsync(string taskId, CancellationToken ct) { - var (tasks, _, settings, ctx) = CreateRepos(); + var (tasks, lists, settings, ctx) = CreateRepos(); await using var __ = ctx; var ok = await tasks.DiscardPlanningAsync(taskId, ct); + + await TryCleanupWorktreeAsync(taskId, lists, settings, ct); + var sessionDir = Path.Combine(_rootDirectory, taskId); if (Directory.Exists(sessionDir)) { - try { Directory.Delete(sessionDir, recursive: true); } - catch { /* best effort */ } + try { Directory.Delete(sessionDir, recursive: true); } catch { } } + if (!ok) throw new InvalidOperationException($"Task {taskId} was not in Planning state; nothing to discard."); } @@ -192,9 +207,49 @@ public sealed class PlanningSessionManager var appSettings = await settings.GetAsync(ct); var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir); - var token = task.PlanningSessionToken ?? string.Empty; + if (!Directory.Exists(worktreePath)) + throw new InvalidOperationException($"Planning worktree missing — cannot resume: {worktreePath}"); - return new PlanningSessionResumeContext(taskId, listWorkingDir, task.PlanningSessionId, token, worktreePath); + var token = await ReadTokenFileAsync(TokenFilePathFor(sessionDir), ct); + + return new PlanningSessionResumeContext( + ParentTaskId: taskId, + WorkingDir: worktreePath, + ClaudeSessionId: task.PlanningSessionId, + Token: token, + WorktreePath: worktreePath); + } + + private async Task TryCleanupWorktreeAsync( + string taskId, + ListRepository lists, + AppSettingsRepository settings, + CancellationToken ct) + { + try + { + var (tasks, _, _, ctx2) = CreateRepos(); + await using var __ = ctx2; + + var task = await tasks.GetByIdAsync(taskId, ct); + if (task is null) return; + + var list = await lists.GetByIdAsync(task.ListId, ct); + var listWorkingDir = list?.WorkingDir; + if (string.IsNullOrEmpty(listWorkingDir) || !Directory.Exists(listWorkingDir)) return; + + var appSettings = await settings.GetAsync(ct); + var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir); + var branchName = BranchNameFor(taskId); + + if (Directory.Exists(worktreePath)) + { + try { await _git.WorktreeRemoveAsync(listWorkingDir, worktreePath, force: true, ct); } + catch { /* best effort */ } + } + try { await _git.BranchDeleteAsync(listWorkingDir, branchName, force: true, ct); } catch { } + } + catch { /* best effort — never block finalize/discard */ } } private static string GenerateToken()