Files
ClaudeDo/docs/superpowers/specs/2026-07-30-empty-repo-hardening-design.md
T
CubeGameLP fea8236e8a docs(git): spec + plan for empty-repo (unborn HEAD) hardening
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5f4b7a9e26af0635cfaecfbc55bfe928dd3466d2)
2026-08-28 20:31:50 +02:00

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.BuildForTaskAsyncWorktreeManager.CreateAsyncIsGitRepoAsync (passes — rev-parse --git-dir works on an empty repo) → ResolveBaseCommitAsyncRevParseHeadAsync (fatals — HEAD is unborn). The same gap exists in PlanningSessionManager.StartAsync and hits autonomous runs (TaskRunnerCreateAsync → 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 HEAD fails → repo is corrupt (detached HEAD onto a missing commit, mangled .git/HEAD) → throw InvalidOperationException with 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):
    1. git hash-object -w -t tree --stdin with empty stdin → empty tree object.
    2. 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.
    3. 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 the IsGitRepoAsync guard, call EnsureHeadCommitAsync; log when a bootstrap happened. Covers autonomous runs, interactive ConPTY sessions, improvement/planning children.
  • PlanningSessionManager.StartAsync — same, before its own RevParseHeadAsync.

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 fresh git init, true after a commit.
  • EnsureHeadCommitAsync on an empty repo: returns true, rev-parse HEAD then 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/HEAD at a bogus ref? no — detach onto a garbage SHA by writing .git/HEAD directly): clear error, no bootstrap.
  • CommitAsync identity fallback: local user.name/user.email set to empty strings (forces the identity failure deterministically regardless of global config) → commit succeeds via fallback, author is ClaudeDo.
  • WorktreeManager.CreateAsync on an empty repo: succeeds; worktree exists; base commit = bootstrap commit.
  • PlanningSessionManager.StartAsync on an empty repo: succeeds (if existing test infra makes this cheap; otherwise covered by the shared code path).