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:
2026-08-28 14:15:23 +04:00
parent 7700dd60c6
commit 4b320809bd
2 changed files with 58 additions and 2 deletions
+11 -2
View File
@@ -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"}} {
+47
View File
@@ -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)
}
}