Exclude the control directory, not the ignored marker inside it

F42, live on run 5's completion tail. The review agent wrote .orchestra/done,
the worker recognised it, confirmed the agent idle, and then failed the result
commit on every attempt:

    stage result: The following paths are ignored by one of your .gitignore
    files: .orchestra/done

.orchestra carries a .gitignore of "*" (internal/herdr/adapter.go:132), so the
marker is ignored, and git refuses an add whose pathspec names an ignored path.
The exclusion now names the directory. Reproduced against git 2.55.0 in a
scratch repo both ways, and the regression test uses the same constant the
worker passes to git.

The failure retried every five seconds for 22 minutes with the task stuck in
review and nothing observable outside the journal, because each identical error
overwrote the single last_error slot. That is F18, still open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
This commit is contained in:
2026-08-28 03:45:40 +04:00
parent 2417a39a1e
commit dcd9af4806
2 changed files with 68 additions and 1 deletions
+57
View File
@@ -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))
}
}