diff --git a/cmd/orchestra-worker/main.go b/cmd/orchestra-worker/main.go index edd468a..a350959 100644 --- a/cmd/orchestra-worker/main.go +++ b/cmd/orchestra-worker/main.go @@ -962,7 +962,7 @@ func (w *worker) finalize(ctx context.Context, id string, s herdr.Session) (comp if _, err := git(ctx, s.Worktree, "diff", "--quiet", "--", "TASK.md"); err != nil { return completionEvidence{}, errors.New("TASK.md was modified") } - if out, err := git(ctx, s.Worktree, "add", "-A", "--", ".", ":!.orchestra/done"); err != nil { + if out, err := git(ctx, s.Worktree, "add", "-A", "--", ".", stageExclude); err != nil { return completionEvidence{}, fmt.Errorf("stage result: %s: %w", out, err) } if _, err := git(ctx, s.Worktree, "diff", "--cached", "--quiet"); err != nil { @@ -990,6 +990,16 @@ func (w *worker) finalize(ctx context.Context, id string, s herdr.Session) (comp return e, nil } +// stageExclude keeps Orchestra's own control files out of the result commit. +// It names the directory, not the marker inside it: .orchestra carries a +// .gitignore of "*" (herdr/adapter.go), and naming an ignored file in a +// pathspec makes git refuse the whole add with "The following paths are +// ignored by one of your .gitignore files". F42, live on run 5: the review +// agent wrote .orchestra/done, and every completion attempt failed on that +// refusal, once every five seconds, with the task stuck in review. Excluding +// the directory also holds for a worktree that has no inner .gitignore. +const stageExclude = ":!.orchestra" + func (w *worker) renewLeases(ctx context.Context) { if w.executionBackend() == nil { return diff --git a/cmd/orchestra-worker/main_test.go b/cmd/orchestra-worker/main_test.go index a4967c8..f4fcabe 100644 --- a/cmd/orchestra-worker/main_test.go +++ b/cmd/orchestra-worker/main_test.go @@ -1045,3 +1045,60 @@ func deadTmuxBackend(t *testing.T) *herdr.TmuxBackend { } return &herdr.TmuxBackend{Socket: "gone", Binary: stub} } + +// F42, live on run 5: the review agent wrote .orchestra/done, and the result +// commit's staging step named that file in an exclude pathspec. Git refuses the +// whole add when a pathspec names an ignored path, so completion failed every +// five seconds and the task never left review. The marker directory ignores +// itself, so the exclusion has to name the directory. +func TestStageExcludeSurvivesTheIgnoredMarker(t *testing.T) { + repo := t.TempDir() + run := func(args ...string) (string, error) { + cmd := exec.Command("git", args...) + cmd.Dir = repo + out, err := cmd.CombinedOutput() + return string(out), err + } + for _, args := range [][]string{ + {"init", "-q", "."}, + {"config", "user.email", "t@example.invalid"}, + {"config", "user.name", "t"}, + } { + if out, err := run(args...); err != nil { + t.Fatalf("git %v: %s: %v", args, out, err) + } + } + if err := os.WriteFile(filepath.Join(repo, "a.txt"), []byte("one\n"), 0o644); err != nil { + t.Fatal(err) + } + if out, err := run("add", "a.txt"); err != nil { + t.Fatalf("%s: %v", out, err) + } + if out, err := run("commit", "-qm", "init"); err != nil { + t.Fatalf("%s: %v", out, err) + } + if err := os.WriteFile(filepath.Join(repo, "a.txt"), []byte("one\ntwo\n"), 0o644); err != nil { + t.Fatal(err) + } + control := filepath.Join(repo, ".orchestra") + if err := os.MkdirAll(control, 0o755); err != nil { + t.Fatal(err) + } + // Exactly what the adapter writes, and the marker the agent writes. + if err := os.WriteFile(filepath.Join(control, ".gitignore"), []byte("*\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(control, "done"), nil, 0o644); err != nil { + t.Fatal(err) + } + if out, err := run("add", "-A", "--", ".", stageExclude); err != nil { + t.Fatalf("staging refused the ignored marker: %s: %v", out, err) + } + staged, err := run("diff", "--cached", "--name-only") + if err != nil { + t.Fatalf("%s: %v", staged, err) + } + if strings.TrimSpace(staged) != "a.txt" { + t.Fatalf("staged %q, want only a.txt", strings.TrimSpace(staged)) + } +}