From e0748ba351645215332f1bc5a81f097e7aa3c947 Mon Sep 17 00:00:00 2001 From: CubeGameLP <126233386+CubeGameLP@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:03:42 +0200 Subject: [PATCH] fix(worktree): auto-bootstrap commit-less repos before creating worktrees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers autonomous runs, interactive ConPTY sessions (GetInteractiveLaunchSpec), planning sessions, and improvement/planning children — every path that needs a base commit now self-heals on a fresh 'git init' repo instead of surfacing "ambiguous argument 'HEAD'" as a HubException. Co-Authored-By: Claude Fable 5 (cherry picked from commit ec16a85c495ceceaa22f922e077a27dcda54f035) --- .../Planning/PlanningSessionManager.cs | 3 ++ src/ClaudeDo.Worker/Runner/WorktreeManager.cs | 4 +++ .../Runner/WorktreeManagerTests.cs | 28 +++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs b/src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs index a3e1b554..84fc9b2a 100644 --- a/src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs +++ b/src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs @@ -98,6 +98,9 @@ public sealed class PlanningSessionManager if (!await _git.IsGitRepoAsync(listWorkingDir, ct)) throw new InvalidOperationException($"Working directory is not a git repository: {listWorkingDir}"); + // A fresh `git init` repo has no HEAD commit to base the planning worktree on. + await _git.EnsureHeadCommitAsync(listWorkingDir, ct); + var appSettings = await settings.GetAsync(ct); var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir); var branchName = BranchNameFor(taskId); diff --git a/src/ClaudeDo.Worker/Runner/WorktreeManager.cs b/src/ClaudeDo.Worker/Runner/WorktreeManager.cs index 3b9f8de9..26fb6cb2 100644 --- a/src/ClaudeDo.Worker/Runner/WorktreeManager.cs +++ b/src/ClaudeDo.Worker/Runner/WorktreeManager.cs @@ -46,6 +46,10 @@ public sealed class WorktreeManager if (!await _git.IsGitRepoAsync(workingDir, ct)) throw new InvalidOperationException($"working_dir is not a git repository: {workingDir}"); + // A fresh `git init` repo has no HEAD commit to base a worktree on — bootstrap one. + if (await _git.EnsureHeadCommitAsync(workingDir, ct)) + _logger.LogInformation("Repository at {Dir} had no commits; created an empty bootstrap commit", workingDir); + var baseCommit = await ResolveBaseCommitAsync(task, workingDir, ct); // Use the full task id (dashes stripped) in the branch name so // two GUIDs sharing an 8-char prefix cannot collide on the same branch. diff --git a/tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs b/tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs index 7a051295..90c964b2 100644 --- a/tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs @@ -191,6 +191,34 @@ public class WorktreeManagerTests : IDisposable Assert.Contains("hello.txt", row.DiffStat); } + [Fact] + public async Task CreateAsync_EmptyRepo_BootstrapsInitialCommit() + { + if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; } + + // Fresh `git init`, zero commits, no identity configured — the cross-PC failure + // that surfaced as "git rev-parse HEAD failed: ambiguous argument 'HEAD'". + var repoDir = Path.Combine(Path.GetTempPath(), $"claudedo_emptyrepo_{Guid.NewGuid():N}"); + Directory.CreateDirectory(repoDir); + GitRepoFixture.RunGit(repoDir, "init", "-b", "main"); + _tempDirs.Add(repoDir); + + var (task, list) = MakeEntities(repoDir); + var (mgr, db) = await CreateManagerAsync(task, list); + + var ctx = await mgr.CreateAsync(task, list, CancellationToken.None); + _worktreeCleanups.Add((repoDir, ctx.WorktreePath)); + + Assert.True(Directory.Exists(ctx.WorktreePath)); + // The base commit is the auto-created bootstrap commit. + var head = GitRepoFixture.RunGit(repoDir, "rev-parse", "HEAD").Trim(); + Assert.Equal(head, ctx.BaseCommit); + + using var readCtx = db.CreateContext(); + var row = await new WorktreeRepository(readCtx).GetByTaskIdAsync(task.Id); + Assert.Equal(WorktreeState.Active, row!.State); + } + [Fact] public async Task CreateAsync_NonGitDir_Throws_NoRow() {