fix(orchestrator): wire TASK.md writing and §6.2 pickup validation (B6, partial)
Fixes AUDIT.md's B6: nothing wrote a TASK.md into a worktree, so continuity.ValidatePickup had no caller and no file to check. - continuity.RenderTaskFile/TaskFileHash: render and hash the immutable §6.2 TASK.md from a domain.Task. - GitWorktrees.Create writes and commits TASK.md into every freshly created worktree (must be committed, not dirty, for ScratchCommit's immutability check and for a stable hash). - Coordinator.Start now runs continuity.ValidatePickup (anchor SHA, dirty-file hashes, TASK.md hash) against the real worktree before bootstrapping a successor onto a handoff_ref, and blocks the task instead of bootstrapping on a validation failure. Still open from Phase 4: handoff production (agent writing the real handoff; Release still refuses per B5), ScratchCommit wiring before release, and the §6.2 bootstrap-prompt rewrite — see AUDIT.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
This commit is contained in:
@@ -2,6 +2,7 @@ package orchestrator_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"orchestra/internal/continuity"
|
||||
"orchestra/internal/domain"
|
||||
"orchestra/internal/orchestrator"
|
||||
"os"
|
||||
@@ -61,3 +62,62 @@ func TestPerProjectGitWorktreesResolvesByProject(t *testing.T) {
|
||||
t.Fatalf("expected unconfigured project to use default worktree root, got %s", pathDefault)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitWorktreesCommitsTaskFile guards AUDIT.md's B6: nothing wrote a
|
||||
// TASK.md into a worktree in the first place, so §6.2 pickup validation had
|
||||
// nothing to check. GitWorktrees.Create must now write and commit an
|
||||
// immutable TASK.md whose on-disk hash matches continuity.RenderTaskFile.
|
||||
func TestGitWorktreesCommitsTaskFile(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
repo := filepath.Join(base, "repo")
|
||||
initRepo(t, repo)
|
||||
|
||||
w := orchestrator.GitWorktrees{Root: filepath.Join(base, "wt"), Repo: repo}
|
||||
task := domain.Task{ID: "t1", Project: "p", Source: "jsonl", ExternalID: "1", Title: "do the thing"}
|
||||
|
||||
path, err := w.Create(context.Background(), task)
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
|
||||
want := continuity.RenderTaskFile(task)
|
||||
got, err := os.ReadFile(filepath.Join(path, "TASK.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("read TASK.md: %v", err)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("TASK.md content mismatch:\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
|
||||
status, err := exec.Command("git", "-C", path, "status", "--porcelain", "--", "TASK.md").Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(status) != 0 {
|
||||
t.Fatalf("TASK.md not committed, status: %s", status)
|
||||
}
|
||||
|
||||
sha, err := continuity.TaskFileHash(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := continuity.VerifyTaskFile(path, sha); err != nil {
|
||||
t.Fatalf("VerifyTaskFile: %v", err)
|
||||
}
|
||||
|
||||
// Re-creating (path already exists) must not touch the committed file.
|
||||
path2, err := w.Create(context.Background(), task)
|
||||
if err != nil {
|
||||
t.Fatalf("recreate: %v", err)
|
||||
}
|
||||
if path2 != path {
|
||||
t.Fatalf("recreate returned different path: %s vs %s", path2, path)
|
||||
}
|
||||
got2, err := os.ReadFile(filepath.Join(path, "TASK.md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got2) != string(want) {
|
||||
t.Fatalf("TASK.md changed on recreate")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user