fix(agent/git): seed baseline commit for empty repos so results push back (#73)
Binding an empty repo (no commits) made `git clone` land an unborn HEAD with no `origin/main`. `add_task_worktree` / `create_result_branch` then fell back to base_ref=HEAD, and `git worktree add ... HEAD` failed with "invalid reference: HEAD". The agent silently degraded to "executing on repo root without git push", so the result branch never reached the remote. Fix: after clone (and after refreshing an existing checkout), detect an empty repo via `git rev-parse --verify HEAD` and plant one empty baseline commit on the configured base branch. Worktree/branch creation and push then work normally and the result branch is created on the (previously empty) remote. Adds scripts/test-git-empty-repo.py covering the empty-repo path; existing scripts/test-git-workflow.py still passes (no regression). Verified end-to-end against a real empty GitHub repo: result branch pushed successfully. Impact: Swarm agent git layer only (agent/git_operations.py). No change to Manager<->Swarm contract, callbacks, billing, secret_ref, or audit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5cf3b88c09
commit
c38b5fd80d
@@ -89,6 +89,7 @@ class GitOperations:
|
||||
|
||||
logger.info(f"Successfully cloned repository to {self.workspace_dir}")
|
||||
await self._configure_git_user()
|
||||
await self._ensure_baseline_commit()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -135,6 +136,48 @@ class GitOperations:
|
||||
logger.error(f"Git clean failed: {clean.stderr}")
|
||||
return False
|
||||
|
||||
await self._ensure_baseline_commit()
|
||||
return True
|
||||
|
||||
async def _has_commits(self, cwd: str) -> bool:
|
||||
"""True if the checkout has at least one commit (a valid, born HEAD)."""
|
||||
result = await self._run_git_command(
|
||||
["git", "rev-parse", "--verify", "--quiet", "HEAD"], cwd=cwd
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
async def _ensure_baseline_commit(self) -> bool:
|
||||
"""Seed an initial commit when the clone is an empty repo (unborn HEAD, no commits).
|
||||
|
||||
An empty remote has no default branch and no commits, so the clone's HEAD is an
|
||||
unborn ref. Downstream `git worktree add ... HEAD` / `checkout -B ... HEAD` then fail
|
||||
with "invalid reference: HEAD", and the agent silently degrades to "without git push",
|
||||
so results never reach the remote (agent_swarm#73). Planting one empty baseline commit
|
||||
on the configured base branch gives those operations a valid base and lets the result
|
||||
branch push back normally. No-op when the repo already has commits. Returns True if the
|
||||
repo ends up with a born HEAD.
|
||||
"""
|
||||
cwd = await self.repo_root() or self.workspace_dir
|
||||
if await self._has_commits(cwd):
|
||||
return True
|
||||
|
||||
logger.warning(
|
||||
"Cloned repository is empty (no commits); seeding baseline commit on '%s' "
|
||||
"so task worktrees can be created and pushed (agent_swarm#73)",
|
||||
self.base_branch,
|
||||
)
|
||||
# Land the first commit on the configured base branch even though HEAD is still unborn.
|
||||
await self._run_git_command(["git", "checkout", "-B", self.base_branch], cwd=cwd)
|
||||
commit = await self._run_git_command(
|
||||
["git", "commit", "--allow-empty", "-m",
|
||||
"chore: initialize empty repository baseline"],
|
||||
cwd=cwd,
|
||||
)
|
||||
if commit.returncode != 0:
|
||||
logger.error(f"Failed to seed baseline commit for empty repository: {commit.stderr}")
|
||||
return False
|
||||
|
||||
logger.info(f"Seeded baseline commit on branch '{self.base_branch}' for empty repository")
|
||||
return True
|
||||
|
||||
async def create_result_branch(self, task_id: Optional[str] = None) -> bool:
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression for agent_swarm#73: binding an EMPTY repo (no commits) must still
|
||||
create a task worktree and push results back, instead of silently degrading to
|
||||
"without git push". See agent/git_operations.py:_ensure_baseline_commit.
|
||||
"""
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
GIT_OPS_PATH = ROOT / "agent" / "git_operations.py"
|
||||
|
||||
spec = importlib.util.spec_from_file_location("git_operations", GIT_OPS_PATH)
|
||||
git_operations = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(git_operations)
|
||||
GitOperations = git_operations.GitOperations
|
||||
|
||||
|
||||
def run(command: list[str], cwd: Optional[Path] = None):
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=cwd,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"Command failed: {' '.join(command)}\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
async def main():
|
||||
with tempfile.TemporaryDirectory(prefix="swarm-git-empty-") as tmp:
|
||||
tmpdir = Path(tmp)
|
||||
remote = tmpdir / "remote.git"
|
||||
workspace = tmpdir / "workspace"
|
||||
worktree = tmpdir / "task-worktree"
|
||||
|
||||
# An EMPTY bare remote: no commits, no default branch — exactly the #73 scenario.
|
||||
run(["git", "init", "--bare", "-b", "main", str(remote)])
|
||||
|
||||
os.environ["GIT_BASE_BRANCH"] = "main"
|
||||
git_ops = GitOperations(str(workspace), "test-agent")
|
||||
|
||||
assert await git_ops.clone_workspace(str(remote)), "clone of empty repo failed"
|
||||
assert await git_ops.is_git_workspace()
|
||||
|
||||
# Baseline must have been seeded so HEAD is now valid.
|
||||
head = subprocess.run(
|
||||
["git", "rev-parse", "--verify", "--quiet", "HEAD"],
|
||||
cwd=workspace, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
)
|
||||
assert head.returncode == 0 and head.stdout.strip(), "empty repo still has unborn HEAD"
|
||||
|
||||
# The previously-failing path: worktree add on an empty repo.
|
||||
branch = await git_ops.add_task_worktree(str(worktree), "task-empty")
|
||||
assert branch, "add_task_worktree returned None on empty repo (regression #73)"
|
||||
|
||||
# Do real work in the worktree and push it back.
|
||||
task_git = GitOperations(str(worktree), "test-agent")
|
||||
task_git.result_branch = branch
|
||||
(worktree / "hello.py").write_text("print('hello from agent')\n", encoding="utf-8")
|
||||
|
||||
commit_sha = await task_git.commit_changes("Task task-empty: add hello")
|
||||
assert commit_sha, "commit failed in empty-repo worktree"
|
||||
|
||||
pushed = await task_git.push_results()
|
||||
assert pushed == branch, "push_results failed for empty-repo worktree"
|
||||
|
||||
refs = run(["git", "for-each-ref", "--format=%(refname:short)", "refs/heads"], cwd=remote)
|
||||
assert branch in refs.stdout.splitlines(), "result branch missing on remote"
|
||||
|
||||
await git_ops.remove_task_worktree(str(worktree))
|
||||
|
||||
print("Empty-repo git workflow test passed")
|
||||
print(f"branch={branch}")
|
||||
print(f"commit={commit_sha}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user