Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 5f4b7a9e26af0635cfaecfbc55bfe928dd3466d2)
4.8 KiB
Empty-repo hardening (unborn HEAD) — design
Date: 2026-07-30 Status: approved (option "Auto-Bootstrap" chosen by Mika)
Problem
GetInteractiveLaunchSpec (embedded ConPTY session) fails with a raw git error when the
task's list points at a git repository that has no commits yet (fresh git init,
unborn HEAD):
HubException: git rev-parse HEAD failed (exit 128): fatal: ambiguous argument 'HEAD': unknown revision ...
Root cause chain: InteractiveLaunchSpecService.BuildForTaskAsync → WorktreeManager.CreateAsync
→ IsGitRepoAsync (passes — rev-parse --git-dir works on an empty repo) →
ResolveBaseCommitAsync → RevParseHeadAsync (fatals — HEAD is unborn). The same gap
exists in PlanningSessionManager.StartAsync and hits autonomous runs (TaskRunner →
CreateAsync → task Failed with the same cryptic message).
This is a "works on my laptop" class of bug: on a fresh PC the first thing a user tries is
a freshly init-ed test repo, which breaks instantly. A second member of the same class:
a PC without git config user.name/email breaks every auto-commit
("Please tell me who you are").
Decision
Auto-bootstrap: when a worktree (task run / ConPTY / planning) needs a base commit and the repo has zero commits, ClaudeDo creates an empty initial commit automatically and proceeds. Nothing can be lost — the repo is empty. Genuinely broken states (HEAD not a symbolic ref and not resolvable) still produce a clear, actionable error instead of raw git stderr.
Design
1. GitService.HasHeadCommitAsync(dir)
git rev-parse --verify --quiet HEAD → exit 0 means HEAD resolves to a commit.
2. GitService.EnsureHeadCommitAsync(dir) → bool (true = bootstrapped)
- HEAD resolves → return
false(no-op). git symbolic-ref -q HEADfails → repo is corrupt (detached HEAD onto a missing commit, mangled.git/HEAD) → throwInvalidOperationExceptionwith a clear message naming the directory. Auto-fixing a corrupt repo is out of scope by design.- Otherwise HEAD is an unborn branch → bootstrap via plumbing only (never touches the
user's index or working tree — staged/untracked files stay exactly as they were):
git hash-object -w -t tree --stdinwith empty stdin → empty tree object.git -c user.name=ClaudeDo -c user.email=claudedo@local commit-tree <tree> -m "chore: initialize repository (ClaudeDo)"— inline identity so this works on a PC with no git identity configured.git update-ref <unborn-branch-ref> <commit>.
- Serialized behind a process-wide gate with a re-check after acquiring, so two parallel task starts on the same empty repo bootstrap exactly once.
3. Defense-in-depth: friendly RevParseHeadAsync error
When rev-parse HEAD fails with the unborn-HEAD signature ("ambiguous argument 'HEAD'"),
throw "The repository at {dir} has no commits yet" instead of raw stderr — covers any
future call site that forgets the preflight.
4. Call sites
WorktreeManager.CreateAsync— after theIsGitRepoAsyncguard, callEnsureHeadCommitAsync; log when a bootstrap happened. Covers autonomous runs, interactive ConPTY sessions, improvement/planning children.PlanningSessionManager.StartAsync— same, before its ownRevParseHeadAsync.
5. Git identity fallback for auto-commits
GitService.CommitAsync: if the commit fails with the missing/empty-identity signature,
retry once with inline -c user.name="ClaudeDo" -c user.email="claudedo@local".
Happy path costs nothing (no preflight call); a fresh PC without git identity no longer
fails every task auto-commit.
Out of scope
- UI validation when configuring a list's WorkingDir (nice-to-have; the worktree paths now either self-heal or produce clear errors).
- Repairing corrupt repos.
Test plan (real git, Worker.Tests)
HasHeadCommitAsync: false on freshgit init, true after a commit.EnsureHeadCommitAsyncon an empty repo: returns true,rev-parse HEADthen works, tree of the bootstrap commit is empty, second call returns false.- Staged-file preservation: file staged in empty repo → after bootstrap it is still staged and NOT part of the bootstrap commit.
- Corrupt HEAD (point
.git/HEADat a bogus ref? no — detach onto a garbage SHA by writing.git/HEADdirectly): clear error, no bootstrap. CommitAsyncidentity fallback: localuser.name/user.emailset to empty strings (forces the identity failure deterministically regardless of global config) → commit succeeds via fallback, author is ClaudeDo.WorktreeManager.CreateAsyncon an empty repo: succeeds; worktree exists; base commit = bootstrap commit.PlanningSessionManager.StartAsyncon an empty repo: succeeds (if existing test infra makes this cheap; otherwise covered by the shared code path).