summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-09 05:08:46 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-09 05:08:53 +0000
commit5c8562460fc5b78372a1cfdb400e0cb1f51875cd (patch)
treef94a0ca9939b98eb472728c0bca6da230b5516cc /internal
parent7c7dd2bc352c91963ece06f3176a032ee7ab462f (diff)
executor: fix sandbox teardown — remove working copy pull, retry push on concurrent rejection
- Remove git pull into project_dir: working copy is the developer workspace and should be pulled manually; www-data can't write to root-owned .git/objects - On non-fast-forward push rejection (concurrent task pushed first), fetch and rebase then retry once instead of failing the entire task
Diffstat (limited to 'internal')
-rw-r--r--internal/executor/claude.go34
1 files changed, 21 insertions, 13 deletions
diff --git a/internal/executor/claude.go b/internal/executor/claude.go
index 2faeff3..ad1c4e3 100644
--- a/internal/executor/claude.go
+++ b/internal/executor/claude.go
@@ -190,10 +190,13 @@ func setupSandbox(projectDir string) (string, error) {
return tempDir, nil
}
-// teardownSandbox verifies the sandbox is clean, pushes new commits to the
-// canonical remote (the bare repo), then fast-forward pulls them into the
-// working copy. This avoids writing git objects directly into the working copy
-// which causes permission errors on shared/mixed-owner repos.
+// teardownSandbox verifies the sandbox is clean and pushes new commits to the
+// canonical bare repo. If the push is rejected because another task pushed
+// concurrently, it fetches and rebases then retries once.
+//
+// The working copy (projectDir) is NOT updated automatically — it is the
+// developer's workspace and is pulled manually. This avoids permission errors
+// from mixed-owner .git/objects directories.
func teardownSandbox(projectDir, sandboxDir string, logger *slog.Logger) error {
// Fail if agent left uncommitted changes.
out, err := exec.Command("git", "-C", sandboxDir, "status", "--porcelain").Output()
@@ -204,7 +207,7 @@ func teardownSandbox(projectDir, sandboxDir string, logger *slog.Logger) error {
return fmt.Errorf("uncommitted changes in sandbox (agent must commit all work):\n%s", out)
}
- // Check whether there are any new commits.
+ // Check whether there are any new commits to push.
ahead, err := exec.Command("git", "-C", sandboxDir, "rev-list", "--count", "origin/HEAD..HEAD").Output()
if err != nil {
logger.Warn("could not determine commits ahead of origin; proceeding", "err", err)
@@ -216,16 +219,21 @@ func teardownSandbox(projectDir, sandboxDir string, logger *slog.Logger) error {
// Push from sandbox → bare repo (sandbox's origin is the bare repo).
if out, err := exec.Command("git", "-C", sandboxDir, "push", "origin", "HEAD").CombinedOutput(); err != nil {
- return fmt.Errorf("git push to origin: %w\n%s", err, out)
- }
-
- // Pull bare repo → working copy.
- src := sandboxCloneSource(projectDir)
- if out, err := exec.Command("git", "-C", projectDir, "pull", "--ff-only", src).CombinedOutput(); err != nil {
- return fmt.Errorf("git pull into project_dir: %w\n%s", err, out)
+ // If rejected due to concurrent push, fetch+rebase and retry once.
+ if strings.Contains(string(out), "fetch first") || strings.Contains(string(out), "non-fast-forward") {
+ logger.Info("push rejected (concurrent task); rebasing and retrying", "sandbox", sandboxDir)
+ if out2, err2 := exec.Command("git", "-C", sandboxDir, "pull", "--rebase", "origin", "master").CombinedOutput(); err2 != nil {
+ return fmt.Errorf("git rebase before retry push: %w\n%s", err2, out2)
+ }
+ if out3, err3 := exec.Command("git", "-C", sandboxDir, "push", "origin", "HEAD").CombinedOutput(); err3 != nil {
+ return fmt.Errorf("git push to origin (after rebase): %w\n%s", err3, out3)
+ }
+ } else {
+ return fmt.Errorf("git push to origin: %w\n%s", err, out)
+ }
}
- logger.Info("sandbox pushed and working copy updated", "sandbox", sandboxDir, "project_dir", projectDir)
+ logger.Info("sandbox pushed to bare repo", "sandbox", sandboxDir)
os.RemoveAll(sandboxDir)
return nil
}