Stop a delivered TASK.md from poisoning every later task
Delivery merges the task branch, and that branch carries the "orchestra: TASK.md" commit. Master therefore ends up holding the previous task's TASK.md, and the next worktree branches from it. writeTaskFile returned early on os.Stat, so it left that inherited file in place. The worker then hashed the current task and every immutability check failed with "TASK.md changed" against a hash for a task nobody was running. Releases failed, rotation never relaunched, phase requests were never read, and the task died on retry_limit without leaving research. Presence is not identity. Compare content, and rewrite when it differs. Found live in run 7, the first task to start after run 6's pull request merged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -88,10 +89,18 @@ func (w GitWorktrees) Create(ctx context.Context, t domain.Task) (string, error)
|
||||
// clean, and so its hash survives independent of any later scratch commits.
|
||||
func writeTaskFile(ctx context.Context, worktree string, t domain.Task) error {
|
||||
path := filepath.Join(worktree, "TASK.md")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
want := continuity.RenderTaskFile(t)
|
||||
// Presence is not identity. Delivery merges the task branch, and the
|
||||
// branch carries this commit, so a later task's worktree branches from a
|
||||
// master that already holds the *previous* task's TASK.md. Returning early
|
||||
// on os.Stat left that file in place, and every immutability check then
|
||||
// failed with "TASK.md changed" against a hash for a task nobody was
|
||||
// running. Found live: run 7 died on retry_limit without ever leaving
|
||||
// research.
|
||||
if got, err := os.ReadFile(path); err == nil && bytes.Equal(got, want) {
|
||||
return nil
|
||||
}
|
||||
if err := os.WriteFile(path, continuity.RenderTaskFile(t), 0644); err != nil {
|
||||
if err := os.WriteFile(path, want, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, args := range [][]string{{"add", "TASK.md"}, {"commit", "-m", "orchestra: TASK.md"}} {
|
||||
|
||||
@@ -125,3 +125,50 @@ func TestGitWorktreesCommitsTaskFile(t *testing.T) {
|
||||
t.Fatalf("TASK.md changed on recreate")
|
||||
}
|
||||
}
|
||||
|
||||
// A delivered task branch carries its own TASK.md, so master ends up holding
|
||||
// the previous task's file. The next worktree must not inherit it: run 7 died
|
||||
// on retry_limit because every release failed "TASK.md changed" against a hash
|
||||
// for a task nobody was running.
|
||||
func TestWorktreeReplacesAnInheritedTaskFile(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
repo := filepath.Join(base, "repo")
|
||||
initRepo(t, repo)
|
||||
|
||||
previous := domain.Task{ID: "previous-task", Project: "p", Title: "the merged one"}
|
||||
if err := os.WriteFile(filepath.Join(repo, "TASK.md"), continuity.RenderTaskFile(previous), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, args := range [][]string{{"add", "TASK.md"}, {"commit", "-m", "merged task branch"}} {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = repo
|
||||
cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git %v: %v: %s", args, err, out)
|
||||
}
|
||||
}
|
||||
|
||||
current := domain.Task{ID: "current-task", Project: "p", Title: "the one being run"}
|
||||
w := orchestrator.GitWorktrees{Root: filepath.Join(base, "wt"), Repo: repo}
|
||||
path, err := w.Create(context.Background(), current)
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(path, "TASK.md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != string(continuity.RenderTaskFile(current)) {
|
||||
t.Fatalf("worktree inherited a stale TASK.md:\n%s", got)
|
||||
}
|
||||
// Committed, not left dirty: the immutability check reads git status.
|
||||
cmd := exec.Command("git", "status", "--porcelain")
|
||||
cmd.Dir = path
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git status: %v: %s", err, out)
|
||||
}
|
||||
if strings.Contains(string(out), "TASK.md") {
|
||||
t.Fatalf("TASK.md left uncommitted: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user