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:
kami
2026-07-27 21:44:00 +04:00
parent a2c3d040d3
commit 7bcad64398
7 changed files with 289 additions and 3 deletions
+44
View File
@@ -67,6 +67,9 @@ func (w GitWorktrees) Create(ctx context.Context, t domain.Task) (string, error)
if out, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("%s: %w", string(out), err)
}
if err := writeTaskFile(ctx, p, t); err != nil {
return "", err
}
if w.TaskFileSHA != "" {
if err := continuity.VerifyTaskFile(p, w.TaskFileSHA); err != nil {
return "", err
@@ -75,6 +78,27 @@ func (w GitWorktrees) Create(ctx context.Context, t domain.Task) (string, error)
return p, nil
}
// writeTaskFile commits the §6.2 immutable TASK.md into a freshly created
// worktree. It must be committed, not left dirty, so ScratchCommit's
// "TASK.md is immutable" check (which inspects `git status`) sees it as
// 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 {
return nil
}
if err := os.WriteFile(path, continuity.RenderTaskFile(t), 0644); err != nil {
return err
}
for _, args := range [][]string{{"add", "TASK.md"}, {"commit", "-m", "orchestra: TASK.md"}} {
cmd := exec.CommandContext(ctx, "git", append([]string{"-C", worktree}, args...)...)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("%s: %w", string(out), err)
}
}
return nil
}
func (w GitWorktrees) Remove(ctx context.Context, _ domain.Task, path string) error {
if path == "" {
return fmt.Errorf("worktree: path required")
@@ -541,17 +565,37 @@ func (c *Coordinator) Start(ctx context.Context, e domain.Event) error {
if err != nil {
return c.block(t, "worktree: "+err.Error())
}
// Best-effort: TASK.md only exists for worktrees this process can read
// locally (the GitWorktrees path). A herdr-hosted worktree on a remote
// machine (WorktreeCreator path) is the same cross-host gap named in
// AUDIT.md's federation-fork section — not solved here.
taskFileSHA, _ := continuity.TaskFileHash(w)
s, err := a.Lease(ctx, t.ID, w)
if err != nil {
return c.block(t, "lease: "+err.Error())
}
if p.HandoffRef != "" {
// §6.2 pickup validation: never bootstrap a successor onto a handoff
// whose anchor/dirty-file/TASK.md hashes don't match what's actually
// in the worktree. A failure here blocks the task rather than
// silently trusting an unvalidated ref (this is the gap AUDIT.md's
// B6 named as unreached from the live path).
h, err := continuity.Load(p.HandoffRef, c.Store)
if err != nil {
_ = a.Kill(ctx, s)
return c.block(t, "handoff: "+err.Error())
}
if err := continuity.ValidatePickup(w, h, taskFileSHA); err != nil {
_ = a.Kill(ctx, s)
return c.block(t, "pickup: "+err.Error())
}
if err = a.Bootstrap(ctx, s, p.HandoffRef); err != nil {
_ = a.Kill(ctx, s)
return c.block(t, "bootstrap: "+err.Error())
}
}
s.HerdrID = p.HarnessID
s.TaskFileSHA = taskFileSHA
c.mu.Lock()
if c.sessions == nil {
c.sessions = map[string]herdr.Session{}
+61
View File
@@ -136,6 +136,67 @@ func TestRotationEmitsValidReleaseWithAnchorSHA(t *testing.T) {
func mustJSON(v any) []byte { b, _ := json.Marshal(v); return b }
// TestStartBlocksOnInvalidPickup guards AUDIT.md's B6/Phase 4 item 4:
// Coordinator.Start must run §6.2 pickup validation against the real
// worktree before bootstrapping a successor onto a handoff_ref, and refuse
// (TaskBlocked) rather than bootstrap on a mismatched anchor.
func TestStartBlocksOnInvalidPickup(t *testing.T) {
repo := t.TempDir()
run(t, repo, "init")
run(t, repo, "config", "user.email", "t@t")
run(t, repo, "config", "user.name", "t")
run(t, repo, "commit", "--allow-empty", "-m", "init")
s, err := store.Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "t1", Surface: string(authz.System), Payload: mustJSON(map[string]any{
"source": "jsonl", "external_id": "1", "project": "p",
})}); err != nil {
t.Fatal(err)
}
task := s.Tasks()[0]
// A handoff whose anchor doesn't match anything in this fresh repo.
badHandoff := map[string]any{
"meta": map[string]any{"id": "h1", "reason": "manual", "rotation_index": 0},
"anchor": map[string]any{"git_sha": strings0(40, 'a'), "branch": "orchestra/t1"},
"goal": "g", "done_when": []string{"x"}, "action": "prompt", "command": "go test ./...",
}
ref, err := s.PutArtifact(mustJSON(badHandoff))
if err != nil {
t.Fatal(err)
}
a := &fakeAdapter{occupancy: 0}
wt := orchestrator.GitWorktrees{Root: t.TempDir(), Repo: repo}
c := &orchestrator.Coordinator{Store: s, Worktrees: wt, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json"}
leaseEvt, err := s.Lease(task.ID, "h1", time.Minute)
if err != nil {
t.Fatal(err)
}
b, _ := json.Marshal(map[string]string{"harness_id": "h1", "handoff_ref": ref})
leaseEvt.Payload = b
if err := c.Start(context.Background(), leaseEvt); err != nil {
t.Fatal(err)
}
got, ok := s.Task(task.ID)
if !ok || got.State != domain.StateBlocked {
t.Fatalf("expected TaskBlocked on invalid pickup, got state=%v ok=%v", got.State, ok)
}
}
func strings0(n int, c byte) string {
b := make([]byte, n)
for i := range b {
b[i] = c
}
return string(b)
}
// keyedHarnessAdapter reports Session.Harness as the harness kind ("claude"),
// distinct from the herdr instance id ("homesrv-claude") under which it is
// registered in AdapterFactory.Herdrs — reproducing production's real key
+60
View File
@@ -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")
}
}